0-1背包问题:有一个贼在偷窃一家商店时,发现有n件物品,第i件物品价值vi元,重wi磅,此处vi与wi都是整数。他希望带走的东西越值钱越好,但他的背包中至多只能装下W磅的东西,W为一整数。应该带走哪几样东西?这个问题之所以称为0-1背包,是因为每件物品或被带走;或被留下;小偷不能只带走某个物品的一部分或带走同一物品两次。
在分数(部分)背包问题(fractional knapsack problem)中,场景与上面问题一样,但是窃贼可以带走物品的一部分,而不必做出0-1的二分选择。可以把0-1背包问题的一件物品想象成一个金锭,而部分问题中的一件物品则更像金沙。
两种背包问题都具有最优子结构性质。对0-1背包问题,考虑重量不超过W而价值最高的装包方案。如果我们将商品j从此方案中删除,则剩余商品必须是重量不超过W-wj的价值最高的方案(小偷只能从不包括商品j的n-1个商品中选择拿走哪些)。
虽然两个问题相似,但我们用贪心策略可以求解背包问题,而不能求解0-1背包问题,为了求解部分数背包问题,我们首先计算每个商品的每磅价值vi/wi。遵循贪心策略,小偷首先尽量多地拿走每磅价值最高的商品,如果该商品已全部拿走而背包未装满,他继续尽量多地拿走每磅价值第二高的商品,依次类推,直到达到重量上限W。因此,通过将商品按每磅价值排序,贪心算法的时间运行时间是O(nlgn)。
为了说明贪心这一贪心策略对0-1背包问题无效,考虑下图所示的问题实例。此例包含3个商品和一个能容纳50磅重量的背包。商品1重10磅,价值60美元。商品2重20磅,价值100美元。商品3重30磅,价值120美元。因此,商品1的每磅价值为6美元,高于商品2的每磅价值5美元和商品3的每磅价值4美元。因此,上述贪心策略会首先拿走商品1。但是,最优解应该是商品2和商品3,而留下商品1。拿走商品1的两种方案都是次优的。
但是,对于分数背包问题,上述贪心策略首先拿走商品1,是可以生成最优解的。拿走商品1的策略对0-1背包问题无效是因为小偷无法装满背包,空闲空间降低了方案的有效每磅价值。在0-1背包问题中,当我们考虑是否将一个商品装入背包时,必须比较包含此商品的子问题的解与不包含它的子问题的解,然后才能做出选择。这会导致大量的重叠子问题——动态规划的标识。
例子:0-1背包问题。总共有三件物品,背包可容纳5磅的东西,物品1重1磅,价值60元。物品2重2磅,价值100元,物品3重3磅,价值120元。怎么才能最大化背包所装物品的价值。
解答:我们可以得出物品一每磅价值60元,大于物品二的每磅50元和物品3的每磅40元。如果按照贪心算法的话就要取物品1。然而最优解应该取的是物品2和3,留下了1.
在0-1背包问题中不应取物品1的原因在于这样无法将背包填满,空余的空间就降低了货物的有效每磅价值。
我们可以利用动态规划来解0-1背包问题。
假设c[i]表示第i件物品的重量,w[i]表示第i件物品的价值,f[i][j]表示背包容量为j,可选物品为物品1~i时,背包能获得的最大价值。
用动态规划求解即先求出背包容量较小时能获得的最大价值,然后根据背包容量较小时的结果求出背包容量较大时的结果,也就是一个递推的填表过程。
当没有可选物品时,背包能获得的最大价值为0。即表格可初始化为
填表过程(即状态转移方程)是:
"j<c[i]"表示第i件物品的重量大于当前背包的容量j,此时显然不放第i件物品。下面解释上述方程在“其它“情况下的意义:”将前i件物品放入容量为j背包中“这个问题,如果只考虑第i件物品放或者不放,那么就可以转化为只涉及前i-1件物品的问题,即:
1. 如果不放第i件物品,则问题转化为只涉及”前i-1件物品放入容量为j的背包中“
2. 如果放第i件物品,则问题转化为”前i-1件物品放入剩下的容量为j-c[i]的背包中“,此时能获得的最大价值就是f[i-1][j-c[i]]再加上通过放入第i件物品获得的价值获得的价值w[i]。
则在”其他“情况下,f[i][j]就是1、2中最大的那个值。
显然,可以从左下角利用状态转移方程依次逐行填表,得到f[3][5](表示可选物品为1、2、3,且背包容量为5时,能获得的最大价值),可见动态规划的确即为一个递推的过程。填表后如下表所示:
代码如下:
int N = 3, V = 5; //N是物品数量,V是背包容量 int c[4] = {0,1,2,3}; int w[4] = {0.60.100.120}; for (i = 0; i <= V; i++) { //逐行填表,i表示当前可选物品数,j表示当前背包的容量 f[i][0] = 0; for (j = 1; j <= V; j++) { if (j < c[i]) { f[i][j] = f[i-1][j]; } else { f[i][j] = max(f[i-1][j], f[i-1][j-c[i]] + w[i]); } } }
背包问题初始化
求最优解的背包问题中,事实上有两种不太相同的问法。有的题目要求”恰好装满背包“时的最优解,有的题目则没有要求必须把背包装满。这两种问法的区别是求解时的初始化不同。
如果是第一种问法,要求恰好装满背包,那么在初始化时除了f[0][0]为0,其他f[0][1~V]均设为-∞ ,这样就可以包装最终得到的解释一种恰好装满背包的最优解。
如果并没有要求必须是把背包装满,而是只希望价格尽量大,初始化时应该将f[0][1~V]全部设为0.
这是为什么呢?可以这样理解:初始化的f数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可以在什么也不装且价值为0的情况下被”恰好装满“,其他容量的背包均没有合法的解,属于未定义的状态,应该被赋值为-∞。如果背包并非被必须装满,那么任何容量的背包都有一个合法解”什么都不装“,这个解的价值为0,所以初始状态的值也就全部为0了。