0-1背包问题是经典的动态规划的例子,个人觉得这是个非常好的问题,可以用许多方法来解决,而且思路也各有不同。
个人比较推荐这三个博客:http://love-oriented.com/pack/, http://www.wutianqi.com/?p=539, http://www.cnblogs.com/FreeAquar/archive/2011/12/17/2291436.html,将动态规划问题讲解得比较详细。但是里面也难免有些地方不是很清楚。这里我也讲讲我的看法,顺便对第二篇文章做点补充。
先定义0-1背包问题,注意这里的符号与下面解题的前后一致:
有N件物品和一个容量为W的背包, 每种物品均只有一件。第i件物品的重量是w[i],价值是v[i]。求解将哪些物品装入背包可使价值总和最大。
来一个例子:
有3件物品,背包容量为10,物品重量的数组为w={3,4,5}, 价值的数组为v={4,5,6}
例子的好处是可以随时验证我们的想法是否正确
如果一开始不告诉你如何来解0-1背包问题,第一个冒出来的解题思路是什么?
尽可能的放价值大而重量小的物品,按照r[i]=v[i]/w[i]从大到小装物品。等等,用例子验证一下,这里r={4/3,5/4,6/5},也就是应该先装第一个,再装第二个,第三个装不下了,于是,得到的最大价值为4+5=9。……直观上看去,5+6=11才是正解,那么,这种方式无法解决这个问题。好吧,我承认,刚刚耍了你,只不过这个是思维的一种方式。这种方式称为贪心法。贪心法适合解决分数背包问题(即物品可以切割),适用贪心法解决的问题应当有最优子结构以及贪心选择性质。所谓的贪心选择性质,就是每一步都只需要考虑当前问题,而不需要考虑子问题(所以刚刚的想法是第一个冒出来的,因为非常简单,每次都只需要考虑当前的价值/重量最大的物品)。
接下来想的可能就是最简单暴力能够实现的方式,即穷举法一个一个列举所有能进入背包的可能性,将只取1个,到只取2个,到只取n个的所有可能算出来,由于这里3个会撑爆背包,所以下图未列出3个的组合,计算完毕之后按照价值倒序排列就是想要的结果。可以看到,这里的计算次数是c(3,1) + c(3,2),写成一般形式就是c(n,1)+c(n,2)+...+c(n,n),而这个
结果是1+2+2²+……2^n-1,虽然有可能最后一些结果可以被剪掉(比如这里的3-3),但是可以想象,当n增大时无论时间空间都是不允许的。
好吧,想想看这里有什么问题?
这个时候,应当想到,如何将原问题分解为子问题,这是一种比较常用的思想,运用这种思想的两大类算法就是分治法和动态规划,因此非常的牛X。可以想到的一种分解子问题的方式是:使用当前物品后重量为W的解空间与不使用当前物品重量就为W的解空间中较大的一个。于是,依据这种子结构,可以将问题用递归实现出来,这里简单写了一个。
public class Pack {
private static int packDp(int start, int maxW, int wei, int[] weight,
int[] value) {
if (start >= weight.length )
return 0;
int nextStart = start + 1;
int valueIn = value[start]
+ packDp(nextStart, maxW, wei + weight[start], weight,
value);
int valueNotIn = packDp(nextStart, maxW, wei, weight, value);
if(valueIn > valueNotIn){
if(maxW < wei + weight[start]){
return 0;
} else{
return valueIn;
}
} else{
if (maxW < wei) {
return 0;
} else{
return valueNotIn;
}
}
}
public static void main(String[] args) {
int[] weight = { 3, 4, 5 };
int[] value = { 4, 5, 6};
int maxW = 10;
System. out.println(packDp(0, maxW, 0, weight, value));
}
}
可以将这个递归过程用图简单画出来,以上面例子为例,最初是求1,2,3这个问题域,但是第一次划分就将问题化为两个子问题,左分支是物品1在解里达到10的重量的情况,右分支是物品1不在解里面达到10的重量的情况,这两种情况取大者就是问题的实际价值。线段上的1表示物品编号,3表示物品1的重量,7表示除去物品1当前剩余重量。而左分支选择以后的实际价值就变成物品1的价值+packDp(2,3)这个子问题的价值,即4+packDp(2,3)。
使用动态规划的两个特征就是最优子结构以及子问题重叠。而动态规划的精髓就在于使用了额外的存储保存了更小的子问题。这种保存有两种方式。
一是自顶向下,典型的就如上面的递归,如果要使用递归来实现同样的问题,则必须使用备忘机制来保存中间的子问题结果(例如一个hashtable),以便下次计算时直接取得。
二是自底向上,典型的方式就是迭代,迭代通常使用数组一步一步的向上保存子问题的解,上层每一步都会使用已经计算出来的子问题解。
下面是动态规划迭代法实现的过程,f[r]表示将前i件物品放入容量为w的背包时的价值,而到底放入第几件是由循环次数i来决定的。这里,我也推荐手动计算一下以便理解这个过程。有不理解可以参考第二篇博客
for i=1..N
for r=W..0
f[r]=max{f[r],f[r-w[i]]+v[i]};
实际上,后面还可以用回溯法跟分支限界法来解决,可以参考推荐的第三篇博客。
最后了,想起一个比较有趣的题目:有n级台阶,每次可以跳1级,2级……直到n级,那么跳这n级台阶一共有几种跳法?
这个题比较有趣,借助于这个问题,可以帮我们梳理下动态规划法的精髓。
参考:
http://love-oriented.com/pack/
http://www.wutianqi.com/?p=539
http://www.cnblogs.com/FreeAquar/archive/2011/12/17/2291436.html