Knapsack Problem分为两种情况:
1. 0-1 Knapsack Problem, 既0-1背包问题。
2. Fractional Knapsack Problem, 既部分背包问题。
下面将分析这两种问题以便加深对此的理解。
问题描述:一个人准备用一个最多能装W重量物品的包来装东西。东西总计有n个,其中第i个物品的重量是W[i],价值是V[i],问这个人最多能带价值多少的东西?
1. 0-1背包问题:在此类背包问题中,此人不能将物品分割为更小的部分。所以对于物品i,此人只能决定带走或者不带走(二元选择),而不能选择带走一部分。
2. 部分背包问题:在此类背包问题中,此人可以带走某物品的一部分,也就是说,物品可以被分割成更小的部分,而此人可以决定带走物品i的xi部分,0≤xi≤1。、
我们可以把0-1背包问题想象为一个人要带走一些贵金属块。而部分背包问题想象为一个人要带走一些贵金属沙。
对于0-1背包问题:
1.不存在贪婪选择性质(greedy choice property),因此不存在贪婪算法。
2.存在最优子结构(optimal substructure property)
3.存在相互重叠的子问题(overlapping subproblem)
所以0-1背包问题可以用动态规划的方法解决(Dynamic programming)。
对于部分背包问题:
1.存在贪婪选择性质
2.存在最优子结构
因此存在贪婪算法(Greedy algorrithm)
用v[1...n]和w[1...n]分别表示n件物品的价值和重量,W表示背包可以装的物品的总重量
I.首先看0-1背包问题
设S为背包可装重量为W时的最优解,i为这个最优解中的最大物品编号。那么S'=S-{i}是背包可装重量为W-w[i]时的最优解,最优解S对应的物品价值为v[i]加上子问题S'对应的物品总价值。
同时考虑到对于任意一件物品i,有两种可能性。(1):i被包含在问题的最优解中 (2):i没有被包含在最优解中。因此n件物品,W重量对应的最大价值是以下两种情况中较大的那个:
(1) n-1件物品,W重量对应的价值的最大值。(也就是说第n件物品没有被包含在最优解中)
(2) n-1件物品,W-w[n]重量对应的价值的最大值+v[n]。(也就是说第n件物品被包含在最优解中,需要解决的问题就变成了其子问题)
如果w[n]>W,则第n件物品一定没有被包含在最优解中,此时只存在第一种可能。
根据以上事实,我们可以得出如下公式:(令c[i,w]表示当重量为W时,物品1...i对应的可装的最大价值。)
0 if i == 0 or w == 0
c[i,w] = c[i-1, W] if w[i]>W
max{c[i-1, W-w[i]]+v[i], c[i-1, W]} if>0 and w[i]≤W
以上公式表明此人要么选择带第i件物品,此时对应的最大价值为第i件物品的价值v[i]加上i-1件物品与W-w[i]重量下此人可以带的物品的最大价值。或者此人选择不带第i件物品,此时此人还能从1...i-1件物品中挑选W重量的物品,对应的子问题就是i-1件物品与W重量下此人可带的物品的最大价值。
该问题的算法的输入为:背包可带最大重量W,物品价值的数组v[1...n],物品重量的数组w[1...n]。同时使用一个二维数组c[0...n,0...W]来存储问题的最优解。根据以上公式,该二维数组的填充顺序为从左至右,从上至下按行填充。c[n,W]为我们想要得到的答案。
Dynamic_0-1_Knapsack(W, v, w, n):
new array c[0...n,0...W];
for i = 0 to n:
for w = 0 to W:
if (i == 0||w ==0):
c[i,w]=0;
else if (w[i] ≤ W):
c[i,w]=max{c[i-1, W-w[i]]+v[i], c[i-1,W]};
else:
c[i,w]=c[i-1,W];
return c[n,W];
在运行结束后,可以根据二维数组c来重建问题每一步对应的最优解,既对比c[i,W]与c[i-1,W],若二者相等,则第i件物品属于最优解,我们接着考虑c[i-1,W]。否则不属于,我们接着考虑c[i-1, W-w[i]]。
分析:
该算法需要θ(nW)的运行时间,因为c表共有(n+1)(W+1)项,每一项的填充需要θ(1)时间。重构最优解需要θ(n)时间,因为c表共有n行而我们从第n行开始考虑,每次上移一行。
II.再看部分背包问题
部分背包问题和0-1背包问题的主要区别在于部分背包问题中的物品是可以只带一部分的。因此此人可以只带第i件物品的xi份,0≤xi≤1。
所以第i件物品对于总重量和总价值的贡献分别为w[i]*Xi和v[i]*Xi。
在这种情况下,显然背包是可以被完全装满的,否则的话我们就可以继续往背包中装剩余的物品的一部分并且提升背包中物品的总价值。
那么根据问题的条件,我们可以先求出每件物品的单位价值既v[i]/w[i],然后按从大到小的顺序装入背包。这也就是执行贪婪算法的一个过程,优先作出当前看来最优的决定(locallly optimal decision)。
首先证明该问题存在贪婪选择性质:
假设v'/w'是子问题中最大的单位密度,那么对于该子问题,我们有v'/w'>子问题中其余任意v/w。那么v'w/w'>v。那么如果该子问题的最优解不包含全部单位价值最大的物品的重量w',我们可以将该最优解中的任意重量置换为w'没被包含的重量从而得到更优的一个解,所以子问题的最优解一定包含全部w'的重量。
算法如下:
Greedy_Fractional_Knapsack(W,v,w,n):
// we can sort items in monotonically decreasing order of v[i]/m[i] prior to this prodecure
new array x[1...n];
value=0;
weight=0;
for i = 1 to n:
x[i]=0;
while (weight i = best remaining item with the highest v[i]/m[i]; if (weight+w[i]≤W): x[i]=1; weight=weight+w[i]; value=value+v[i] else: x[i]=(W-weight)/w[i]; value=value+v[i]*x[i] weight=W; return value, x; 分析: 如果输入的物品的序列已经被提前按照单位价值递减(v[i]/w[i])的顺序排序,那么算法中的while循环所需要的时间为O(n),那么算上排序的时间,该算法所需要的总时间为O(nlogn)。 如果我们将输入保存在一个以v[i]/w[i]最大的物品作为根的优先队列中: 创建该优先队列需要O(n)时间,while循环需要O(logn)时间(每一次根被取走之后优先队列都需要被重新排列) 虽然应用优先队列并没有提高最坏情况的时间,然而使用优先队列可能能够加快程序在输入较小的情况下的运行时间。 参考文献: http://www.personal.kent.edu/~rmuhamma/Algorithms/MyAlgorithms/Greedy/knapscakFrac.htm http://www.personal.kent.edu/~rmuhamma/Algorithms/MyAlgorithms/Greedy/knapscakFrac.htm