背包问题
问题定义:背包问题
输入 :物品价值 v1,v2,...,vn;物品大小 s1,s2,...,sn;背包容量 C(所有的值均为正整数)。
输出 :一个物品子集 S⊆{1,2,...,n} ,具有最大的价值之和 ∑i∈Svi,但必须满足总大小 ∑i∈Ssi 不超过 C。
考虑下面这个背包问题的实例,背包的容量C= 6并且有4件物品
| 物品 | 价值 | 大小 |
|---|---|---|
| 1 | 3 | 4 |
| 2 | 2 | 3 |
| 3 | 4 | 2 |
| 4 | 4 | 3 |
最优解决方案的总价值是多少呢?
为了在背包问题中应用动态规划,我们必须推断出正确的子问题集合。
为了实现这个目标,我们需要推断出最优解决方案的结构,并确认从更小子问题的最优解决方案构造更大子问题解决方案的不同方式。
这种操作的另一个成果是一个推导公式,它可以从两个更小子问题的解决方案中计算出一个更大子问题的解决方案。
这个最优解决方案看上去应该是怎么样的呢? 我们可以从一个思路出发: S 要么包含了最后一件物品(物品 n),要么不包含它。
背包问题的最优子结构
设 S是具有 n≥1件物品、物品价值 v1,v2,...,vn、物品大小 s1,s2,...,sn、背包容量 C的背包问题的最优解决方案。 则 S必为下面两者之一:
(a)由前 n−1件物品组成的背包容量为 C的子问题的最优解决方案。
(b)由前 n−1件物品组成的背包容量为 C−sn的子问题的最优解决方案再加上最后一件物品 n。
(a)的解决方案总是最优解决方案的选项之一。(b)的解决方案当且仅当 sn≤C时才是选项之一。
在这种情况下,可以有效地预先为物品 n保留 sn个单位的容量。具有更大总价值的那个选项就是最优解决方案,从而形成下面的推导公式。
背包问题的推导公式
根据背包问题的最优子结构 的假设和说明,设 Vi,c表示总大小不超过 c的前 i件物品所组成的子集的最大总价值 (当 i=0时, Vi,c可以看成 0)。对于每个 i=1,2,...,n和 c=0,1,2,...,C:
Vi,c=⎩ ⎨ ⎧情况1 Vi−1,cmax{情况1 Vi−1,c,情况2 Vi−1,c−si+vi}Si>cSi≤c
由于 c和物品的大小都是整数,因此第二个表达式中的剩余容量 c−si也是整数。
下一个步骤是定义相关子问题的集合,并使用背包问题的推导公式系统地解决这些子问题。至于现在,我们把注意力集中在计算每个子问题的最优解决方案的总价值上。
对于背包问题,子问题应该由两个索引进行参数化:前几个物品的长度 i和可用的背包容量 c。对两个参数所有相关的值均加以考虑,我们就可以得到子问题。
背包问题的子问题
计算前 i个物品和背包容量为 c的最优背包解决方案的总价值 Vi,c(i=0,1,2,...,n,c=0,1,2,...,C)。
最大子问题 (i=n且c=C)就与原问题相同。由于所有物品的大小和背包容量 C都是正整数,并且由于容量总是会减去某个物品的大小(为它保留空间),因此剩下的容量只可能在 0~C。
明确了子问题和推导公式之后,我们立即就能想到背包问题的一种动态规划算法。
算法:背包问题
输入 :物品价值 v1,v2,...,vn,物品大小 s1,s2,...,sn和背包容量 C(均为正整数)。
输出 :具有最大总价值的子集 S⊆{1,2,...,n} ,满足总大小 ∑i∈Ssi≤C。
// 子问题的解决方案(索引从0开始)
A:=(n+1)×(C+1)二维数组
// 基本情况( i=0)
for c = 0 to C do
A0c=0
//系统性地解决所有的子问题
for i = 1 to n do
for c = 0 to C do
// 使用背包问题的推导公式
if si > c then
Aic:=Ai−1c
else
Aic:=max{情况1 Ai−1c,情况2 Ai−1c−si+vi}
return AnC
//最大子问题的解决方案
python
def knapsack(weights, values, capacity):
assert len(weights) == len(values), "物品价值数组长度必须等于物品大小数组"
n = len(weights)
A = [[0 for _ in range(capacity+1)] for _ in range(n+1)]
for c in range(capacity+1):
A[0][c] = 0
for i in range(1, n+1):
for c in range(0, capacity+1):
if weights[i-1] > c:
A[i][c] = A[i-1][c]
else:
A[i][c] = max(A[i-1][c], A[i-1][c-weights[i-1]] + values[i-1])
return A[n][capacity]
现在,数组A是一个二维数组,反映了对子问题进行参数化时所使用的索引 i和 c。
当双重for循环的一次迭代必须计算子问题解决方案 Aic时, 两个相关的更小子问题的值 Ai−1c和 Ai−1c−si在外层循环之前的一次迭代时(或作为基本情况)已经计算出来了。 我们可以得出结论,这个算法花费 O(1)的时间解决 (n+1)(C+1)=O(nC)个子问题中的每一个,因此总体运行时间是 O(nC)。
示例
考虑下面这个背包问题的实例,背包的容量C= 6并且有4件物品
| 物品 | 价值 | 大小 |
|---|---|---|
| 1 | 3 | 4 |
| 2 | 2 | 3 |
| 3 | 4 | 2 |
| 4 | 4 | 3 |
最优解决方案的总价值是多少呢?
由于 n=4且 C=6,因此背包算法中的数组 A可以用一个 5列(对应于 i=0,1,...,4) 7行(对应于 c=0,1,...,6)的表格形象地说明。 最终的数组值如图所示。

背包算法(按照从左到右的顺序)可以计算这些项。在同一列中,背包算法则是按照从下到上的顺序进行计算。
为了填充第 i列的某一项,算法把它左边紧邻的那个项(对应于情况1)和" vi与左侧向下 si行的那一项之和"进行比较, 并取两者中较大的那个。
对于 A25而言,更好的选择是跳过后者直接选择左边紧邻的" 3"。
但对于 A35而言,更好的选择是包含物品 3,也就是选择 4( v3)加上 2(左侧向下 si行的那一项,即 A23)。
python
# 示例
weights = [4, 3, 2, 3] # 物品重量
values = [3, 2, 4, 4] # 物品价值
capacity = 6 # 背包容量
max_value = knapsack(weights, values, capacity)
print(f"最大价值为: {max_value}") # 输出: 最大价值为: 8
makefile
最大价值为: 8
重建
背包算法只计算最优解决方案的总价值,并不产生最优解决方案本身。
我们可以通过回溯填充数组A的过程来重新构建一个最优解决方案。
这个重建算法以右上角的最大子问题为起点,确认使用推导公式的某种情况来计算 AnC。
如果是第一种情况,算法就忽略物品 n,并从 An−1C这一项继续重建过程。
如果是第二种情况,算法就在它的解决方案中包含物品 n,并从 An−1C--sn这一项继续重建过程。
背包问题的重建算法
输入 :背包算法为物品价值是 v1,v2,...,vn,物品大小是 s1,s2,...,sn和背包容量是 C的背包问题所计算产生的数组 A。
输出:一个最优的背包问题解决方案。
S:=∅ // 最优解决方案中的物品
c:=C // 剩余的容量
for i = n downto 1 do
if si≤c and Ai−1c−si+vi≥Ai−1c then
S:=S∪{i} // 第一种情况获胜,包含 i
c:=c−si // 为它保留空间
// else 跳过i,容量保持不变
return S
python
def knapsack_with_items(weights, values, capacity):
assert len(weights) == len(values), "物品价值数组长度必须等于物品大小数组"
n = len(weights)
A = [[0 for _ in range(capacity+1)] for _ in range(n+1)]
for c in range(capacity+1):
A[0][c] = 0
for i in range(1, n+1):
for c in range(0, capacity+1):
if weights[i-1] > c:
A[i][c] = A[i-1][c]
else:
A[i][c] = max(A[i-1][c], A[i-1][c-weights[i-1]] + values[i-1])
# 回溯找出选择的物品
S = []
c = capacity
for i in range(n, 0, -1):
if weights[i-1] <= c and A[i-1][c-weights[i-1]] + values[i-1] > A[i-1][c]:
S.append(i) # 添加物品索引,这里是从1开始计算的索引
c -= weights[i-1]
return A[n][capacity], S
对上图所示的数组进行回溯产生最优解决方案{3,4},如图所示。

python
# 示例
weights = [4, 3, 2, 3] # 物品重量
values = [3, 2, 4, 4] # 物品价值
capacity = 6 # 背包容量
max_value, selected_items = knapsack_with_items(weights, values, capacity)
print(f"最大价值为: {max_value}") # 输出: 最大价值为: 8
print(f"选择的物品索引: {selected_items}") # 输出: 选择的物品索引: [3, 2]
makefile
最大价值为: 8
选择的物品索引: [4, 3]
参考文档
《算法详解(卷3)------贪心算法和动态规划》:4.5 背包问题