假设有一个承重量为C的背包。现有n件物品,质量分别为w1,w2,…,wn,价值分别为v1,v2,…,vn,求让背包里装入的物品具有最大的价值总和的物品子集。和矿工挖矿问题类似,在选择装入物品时,对每种物品只有装入或者不装入两种选择。不能将同一个物品装入背包多次,也不能只装入该物品的一部分。
设物品i的质量为wi,价值为vi,1≤i≤n,则已知wi>0,vi>0, C>0。求n元向量{x1,x2,…,xn},其中,xi∈{0,1},0表示不选择该物体,1表示选择该物体。
可以把前i个物品中能够放进承重量为j的背包中的子集分为两种类别:
包括第i个物品的子集和不包括第i个物品的子集。
于是可以得到以下结论:
在不包括第i个物品的子集中,最优子集的价值是F(i-1,j);
在包括第i个物品的子集中(j-wi≥0),最优子集是由该物品和前i-1个物品中能够放进承重量为j-wi的背包的最优子集组成。这种最优子集的总价值等于vi+F(i-1,j-wi)。
问题实例:设书包的最大承重量C=8
参数 | 词典 | 篮球 | 球拍 | 书本 |
w | 2 | 4 | 5 | 3 |
v | 5 | 4 | 6 | 2 |
初始边界条件:
状态转移函数:
i/j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | ||||||||
2 | 0 | ||||||||
3 | 0 | ||||||||
4 | 0 |
背包空间为m,设第i个物品所需的空间为w[i],物品价值v[i]。在到达本物品需要空间前,价值等于前i-1个物品,F(i-1,j)
1,放弃第i个物品,总价值为i-1个物品,F(i-1,j)
2,从j空间中分出w[i]去处理第i个物品,总价值为缺了w[i]个空间的前i-1个物品的价值+第i个物品的价值,F(i-1,j-L[i])+v[i]
F(i,j)=F(i-1,j) (i>1,j<w[i])
F(i,j)=max{F(i-1,j),F(i-1,j-w[i])+v[i]} (i>1,j≥w[i])
当只放第一个物品时,w(1)=2、v(1)=5。
i/j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 5 | 5 | 5 | 5 | 5 | 5 | 5 |
2 | 0 | ||||||||
3 | 0 | ||||||||
4 | 0 |
加上第二个物品,w(2)=4、v(2)=4
i/j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 5 | 5 | 5 | 5 | 5 | 5 | 5 |
2 | 0 | 0 | 5 | 5 | 5 | 5 | 9 | 9 | 9 |
3 | 0 | ||||||||
4 | 0 |
加上第三个物品,w(3)=5、v(3)=6
i/j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 5 | 5 | 5 | 5 | 5 | 5 | 5 |
2 | 0 | 0 | 5 | 5 | 5 | 5 | 9 | 9 | 9 |
3 | 0 | 0 | 5 | 5 | 5 | 6 | 9 | 11 | 11 |
4 | 0 |
加上第四个物品,w(4)=3、v(4)=2
i/j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 5 | 5 | 5 | 5 | 5 | 5 | 5 |
2 | 0 | 0 | 5 | 5 | 5 | 5 | 9 | 9 | 9 |
3 | 0 | 0 | 5 | 5 | 5 | 6 | 9 | 11 | 11 |
4 | 0 | 0 | 5 | 5 | 5 | 7 | 9 | 11 | 11 |
代码:
def backpack_record(n, c, w, v):
# 初始化记录表
backpack_rec = [[0 for _ in range(c + 1)] for _ in range(len(n) + 1)]
for i in range(1, len(n) + 1):
for j in range(1, c + 1):
# 使用状态转移函数填写记录表
if j < w[i - 1]: # 承重量不足,放弃第i个物品
backpack_rec[i][j] = backpack_rec[i - 1][j]
else:
backpack_rec[i][j] = max(backpack_rec[i - 1][j], backpack_rec[i - 1][j-w[i - 1]] + v[i - 1])
return backpack_rec
在确定最优解之后,还需要确定是哪些选择构成了最优解。
根据状态转移函数,可以得到以下回溯方法。
(1)若F(i,j)=F(i-1,j),则说明当前物品没有放到背包中,回溯到F(i-1,j)中。
(2)若F(i,j)=vi+F(i-1,j-wi),则说明当前物品已经放到背包中,将该物品记录为组成最优解的元素,回溯到F(i-1,j-wi)。
(3)重复上述回溯过程,直到i=0,即可获得所有组成最优解的物品集合。
代码:
def backpack_results(n, c, w, res):
print('可容纳最大价值为:', res[len(n)][c])
x = [False for _ in range(len(n) + 1)] # 判断是否装入
j = c # 背包承重量
i = len(n) # 物品数
# 回溯
while i >= 0:
if res[i][j] > res[i - 1][j]: # 大于同样承重i-1个物品,说明第i个物品有放入
x[i] = True # 确定装入
j -= w[i - 1] # w是从0开始的,去掉第i个物品的重量
i -= 1 # 向下搜索
print('选择的物品为:')
for i in range(len(n) + 1):
if x[i]:
print('第', i, '个,', end='')
print('')
输入:
输出: