上次一不小心装了个大的,放出豪言要讲动态规划,结果在组内讲的时候还讲错了,幸好凭借多年演技,沉着冷静,胆大心细,走位逼真,操作细腻,瞬间爆炸,完成反杀。。。额,后边这个没有。终于是没丢人丢到5楼去。
好了,既然是说背包问题,那就先把问题复制过来。
问题描述:
挖掘机技术究竟哪家强?
额。。。不对,是这个。。。
给定n种物品和一背包。物品i的重量是wi,其价值为vi,背包的容量为Weight。问应如何选择装入背包的物品,使得装入背包中物品的总价值最大?
这里有一个前提,物品能不能拆,能拆是一种解法,不能拆又是另一种解法。先说说不能拆的情况。
在不能拆的情况下,各个物品只有两种状态,放or不放,所以这种不能拆的背包又叫做0-1背包问题,放用1表示,不放用0表示。
你想,或者不想它,它就在那里,不言不语。
你放,或者不放它,解就在那里,是零是一。
你拆,或者不拆它,说不定在哪里,看性价比。
来我包里,或者,快到我碗里~~~
好了,开始解我们的问题。
第一种解法看看我们的通用解题法——回溯法。因为本身回溯法的思想是,先试试,不行再回来,这样我们就可以解决大部分的问题了。那么就背包问题来看,回溯法就是,第一次放进去k个物品,得出一个包内的价值,此时背包剩下的重量,不能再放物品了,然后取出一个,换别的第一次没放进去的物品,看一下能不能得出比第一次的价值大的解来,如果不能,就在取出一个来,然后就这样下去。
比如我们第一次放入了123,这三个物品,那么第一次应该拿出3来,然后考虑,456。。。这些放进去能不能有更多的价值。再考虑2拿出来,3456。。。会不会有更多的价值,也就是说,我们取出哪个物品,我们就只考虑背包没有那个物品的解。
这里有一个问题,回溯法求解之前要先按照性价比排序,这里如果不是米老师讲的话我还真没看出来。
所以我们看一些回溯法资料的时候,都是用二叉树的深度优先搜索来表示整个过程的,这个咱们那本书上有,愿意看的看看,相信看了我说的这个过程之后应该就能看懂那个树了。
接下来就是大伙表示很看不懂的代码了。
回溯法中背包问题的代码有两部分,求出解的过程,和计算背包物品价值上界。
public class Pack
{
int[] v; //物品价值数组
int[] w; //物品重量数组
/// <summary>
/// 根据贪心法,计算背包价值上界
/// </summary>
/// <param name="v">当前背包价值</param>
/// <param name="w">当前背包重量</param>
/// <param name="k">已经放进去的物品数</param>
/// <param name="Weight">背包总重量</param>
/// <returns>价值上界</returns>
public double Bound(int cv, int cw, int k, int Weight)
{
int b = cv; //价值上界
int c = cw; //保存当前背包剩余容量
//背包放入物品后,剩余的容量,和剩余容量能获得的最大价值
for (int i = 0; i < k + 1; i++)
{
c = c + w[i]; //已经放入的物品重量
if (c < Weight) //若未超过背包总重量
b = b + v[i]; //已经放入的物品价值
else //超过总重量
{
return (b + (1 - (c - Weight) / w[i] * v[i]));
//加上最后一点容量,能放入部分下一个物品的价值
}
}
return b; //返回价值上界
}
/// <summary>
/// 整个二叉树的回溯过程
/// </summary>
/// <param name="Weight">背包总重量</param>
/// <param name="n">物品个数</param>
/// <param name="w">物品重量数组</param>
/// <param name="v">物品价值数组</param>
/// <param name="MaxW">获得最大价值时的重量</param>
/// <param name="MaxV">获得的最大价值</param>
/// <param name="X">最优解</param>
/// <returns>返回X</returns>
public int[] BKNAP1(int Weight, int n, int[] w, int[] v, int MaxW, int MaxV, int[] X)
{
int cw = 0; //当前背包重量,current weight
int cp = 0; //当前背包价值, current price
int k = 1; //数组下标,从1开始
int[] Y; //解数组,物品k放入背包,则Y[k]=1
MaxV = -1; //最大价值,这里等于几都行,只是一个初始值
while (true)
{
//如果物品能放入背包
while (k < n + 1 && cw + w[k] < Weight + 1)
{
cw = cw + w[k]; //当前重量加上放入物品重量
cp = cp + v[k]; //当前价值加上放入物品价值
Y[k] = 1; //表示物品k放入背包
k = k + 1; //下一个物品
}
if (k > n) //若所有物品都能放入
{
MaxV = cp; //当前价值=最大价值
MaxW = cw; //当前重量等于最大重量
k = n; //k等于物品数
X = Y; //最优解为Y
}
else
{
Y[k] = 0; //若不是所有的都能放入,从不能放入之后开始每个都为0
}
//如果价值上界都小于当前最大价值
while (Bound(cp, cw, k, Weight) < MaxV + 1)
{
//找到无法放入的第K个物品
while (k != 0 && Y[k] != 1)
{
k = k - 1; //往前找
}
if (k == 0) //如果找到最后一个,说明一个物没放
{
return Y;
}
Y[k] = 0; //将当前物品取出
cw = cw - w[k];
cp = cp - v[k];
}
k = k + 1; //判断下一个物品
return X;
}
}
}
伪代码实在是打着麻烦,翻一个熟悉的。书上的代码和我的有点点不一样,一句一句的写注释也是挺难的,在听了米老师的课之后才算是把价值上界想通了。我就不再讲了,最后可能有一点错误,万一我理解错了希望有人指出来。
回溯可能看起来比较抽象,贪心应该就简单多了,就是突出一个硬塞,那个性价比高塞哪个。
贪心的物品有时候是可以拆的,可以拆的时候就要考虑拆哪个更值钱,所以一切的关键还是在算前排序上。
/// <summary>
/// 贪心算法
/// </summary>
/// <param name="v">物品价值数组</param>
/// <param name="w">物品重量数组</param>
/// <param name="weight">背包总重量</param>
/// <param name="x">解</param>
/// <param name="n">物品数</param>
public int[] Greedy(int[] v, int[] w, int weight, int[] x, int n)
{
//初始化解数组
for (int i = 0; i < n; i++)
{
x[i] = 0;
}
int c = weight; //剩余重量
//向背包添加物品
for (int i = 1; i < n + 1; i++)
{
//能放就放
if (w[i] <= c)
{
x[i] = 1; //表示放入
c = c - w[i]; //剩余容量减当前物品容量
}
//如果放不进去了
if (i <= n && w[i]>c)
{
x[i] = c / w[i];//按照剩余容量与物品重量的比例放入
c = 0; //放入之后背包容量为0
}
}
return x; //返回解
}
看起来贪心法简单多了是不是?我这里改了一下书上的算法,书上是没有最后c=0这句话的,但是实际上应该是有的,不信自己写写看~
最后一种是动态规划,虽然当初分配给我的就是这个,我看的也最多,但还是觉得动态规划是最难的,尤其涉及到老师说的那个最优子结构,我看的时候都没想过,只注意了子问题的重叠。
动态规划的代码实际上就是米老师带着我们看的那个表的画表的过程,我当时实在是看不懂那个是什么玩意,就自己手动按照代码画了一个,米老师说的很对,动手画上几行就全懂了。而求解的过程是另一个过程,其实就是个简单的判断,下面看我接着翻译。
/// <summary>
/// 动态规划法
/// </summary>
/// <param name="n">考虑前几个物品</param>
/// <param name="Weight">背包容量</param>
/// <returns>在j的重量下考虑前i个物品时的价值</returns>
public int[,] DynamicProgramming(int n, int Weight)
{
int[,] c; //二维数组,存放在j的容量下考虑前i个物品时的价值
//考虑0个物品时总价值全是0
for (int k = 0; k < Weight + 1; k++)
{
c[0, k] = 0;
}
//从前1个物品开始考虑
for (int i = 1; i < n + 1; i++)
{
c[i, 0] = 0; //背包总重是0时,总价值全是0
//从背包容量为1时开始考虑
for (int j = 1; j < Weight + 1; j++)
{
if (w[i] <= j) //如果当前物品重量,小于背包容量,加入考虑范围
{
if (v[i] + c[i - 1, j - w[i]] >= c[i - 1, j])
//如果当前物品价值,加上未放入该物品时的最大价值,大于等于不放入该物品的价值
{
c[i, j] = v[i] + c[i - 1, j - w[i]];
//那么这个物品放入,当前总价值更新
}
else
{
c[i, j] = c[i - 1, j];
//否则不放入该物品,当前总价值不变
}
}
else //当前物品重量,超过背包容量,不放入考虑范围
{
c[i, j] = c[i - 1, j];
//当前总价值不变
}
}
return c;
}
}
/// <summary>
/// 求解
/// </summary>
/// <param name="c">刚才求出的价值数组</param>
/// <param name="j">考虑背包重量为j</param>
/// <param name="n">物品总数</param>
/// <returns>解</returns>
public int[] OutPut(int[,] c, int j,int n)
{
int[] x; //解数组
//从最后一个物品开始考虑,不考虑第一个物品
for (int i = n; i > 1; i--)
{
//如果考虑第i个物品的价值,等于第i-1个物品,那么表示这个物品没有放入
if (c[i,j]==c[i-1,j])
{
x[i]=0; //表示没放入
}
else
{
x[i] = 1; //表示放入
j=j-w[i]; //当前考虑的重量,减去物品i的重量
}
}
//如果只考虑第一个物品时价值为0,那么表示第一个物品没放入
if (c[1,j]==0)
{
x[1] = 0;
}
else
{
x[1] = 1;
}
}
至于米老师的那种动态规划法,我是没能力在这么短的时间之内把代码写出来,等到软考之后或许会仔细想想。
好了,这次是把以前没写够的长度,一次都写得差不多了,以前确实觉得算法难,但是真的仔细看下去,动手写写那些过程,书上的例子都是很基础的,再次奉劝那些说这个难那个难的同学们,静下心来多看两眼吧。
以上