0-1 背包问题:小偷有一个固定容量的背包,到商店偷东西,每个商品的重量 weight(w)和价格 value (v)都不同。问怎么规划能够偷到最多价格的商品?
B(k,c) 表示当背包剩余容量为 c 的时候,偷 [1,…,k]这前 k 件物品的时候,能够获取的最大价值。
"""
参考:
https://www.bilibili.com/video/av36136952?from=search&seid=9381742344384892478
https://blog.csdn.net/TimeMagician/article/details/79731062
https://www.acwing.com/solution/acwing/content/1374/
【公式】
k表示商品序号
|------> B(k-1,c) 第k件太重 (1
B(k,c) = |
| -> 偷: B(k-1,c-w(k)) + v(k) (2
|--|
-> 不偷:B(k-1,c) (3
【动态规划解法】
===============================================================================================================
[index(k)]
[capacity(c)] 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
no item 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
w 2 v 3 1 0 0 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3
w 3 v 4 2 0 0 3 4 4 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7
w 4 v 5 3 ...
w 5 v 8 4 ...
w 9 v 10 5 ... 26
==============================================================================================================
"""
import numpy as np
def knapsack():
""" 二维 dp """
N = 5 # 物品数量
W = 20 # 背包容量
B = np.zeros((N+1, W+1))
w = [0, 2, 3, 4, 5, 9] # 前面补上0是为了和数组对其,no item 等价于在上面的二维数组中 w0 v0
v = [0, 3, 4, 5, 8, 10]
# k: [1,...,5]
for k in range(1, N+1):
# c : [0,...,20]/[1,...,20] 从0或者1开始都可以 c 表示 capacity
# 如果初始化的时候,只初始化第0行全为0,而不初始化第0列全为0的话,那么可以从(k,c)=(1,0)开始就有意义了,因为(1,0)还是未知数
# 但是如果初始化的时候,已经把整个二维数组都初始化为0的话,那么也就是 第0行和第0列都是全0,那么直接从(1,1)开始就好,也就是c=1开始
for c in range(0, W+1):
if w[k] > c: # 如果第k个的重量已经太重了(第 1)种情况)
B[k][c] = B[k - 1][c]
else:
B[k][c] = max(B[k-1][c], B[k - 1][c - w[k]] + v[k])
print("======================= 2D ============================")
print("B:\n",B)
print("B[5][20]:\n",B[5][20]) # answer = 26
def knapsack_1D():
""" 一维 dp """
N = 5 # 物品数量
W = 20 # 背包容量
B = np.zeros((W+1))
w = [0, 2, 3, 4, 5, 9] # 前面补上0是为了和数组对其,no item 等价于在上面的二维数组中 w0 v0
v = [0, 3, 4, 5, 8, 10]
# k: [1,...,N]
for k in range(1, N+1): # [1,...,5]
for c in range(W, -1, -1): # [20,...,0]
# if w[k] > c: # 如果第k个的重量已经太重了(第 1)种情况)
# B[c] = B[c]
# else:
# B[c] = max(B[c], B[c - w[k]] + v[k])
if w[k] <= c:
B[c] = max(B[c], B[c - w[k]] + v[k])
print("======================= 1D ============================")
print("B:\n",B)
print("B[-1]:\n",B[20]) # answer = 26
if __name__ == '__main__':
knapsack()
knapsack_1D()
【输出】
D:\software_install_position\anaconda3\envs\tf_1_12_0\python.exe "E:/####学习资源/#工作资料 数据结构、计算机基础等/数据结构/刷题LeetCode/DP_Learning/knapsack.py"
======================= 2D ============================
B:
[[ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
0. 0. 0.]
[ 0. 0. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3.
3. 3. 3.]
[ 0. 0. 3. 4. 4. 7. 7. 7. 7. 7. 7. 7. 7. 7. 7. 7. 7. 7.
7. 7. 7.]
[ 0. 0. 3. 4. 5. 7. 8. 9. 9. 12. 12. 12. 12. 12. 12. 12. 12. 12.
12. 12. 12.]
[ 0. 0. 3. 4. 5. 8. 8. 11. 12. 13. 15. 16. 17. 17. 20. 20. 20. 20.
20. 20. 20.]
[ 0. 0. 3. 4. 5. 8. 8. 11. 12. 13. 15. 16. 17. 17. 20. 20. 21. 22.
23. 25. 26.]]
B[5][20]:
26.0
======================= 1D ============================
B:
[ 0. 0. 3. 4. 5. 8. 8. 11. 12. 13. 15. 16. 17. 17. 20. 20. 21. 22.
23. 25. 26.]
B[-1]:
26.0
Process finished with exit code 0
【特别说明】
一维的这种情况,这里的 c 倒序 是因为在二维表格中 B[k - 1][c - w[k]] 位于 B[k][c] 左上方。
类似于常见的 a[i][j]=a[i-1][j-1]的题目的中,要转成一维的话,由于 a[i-1][j-1]位于a[i][j]的左上方。如果 j 从小到大,即从左往右的话,会造成值的覆盖,所以需要搞一个临时变量 pre,当然如果初始化的值足够的话,也可以倒序。
之前这种题目一般不会出现 二维表的第0行整行初始值都知道,所以以往的题目都是用临时变量保存的 比如《No.72 编辑距离》。
而倒序就不会造成覆盖了,所以这里同理,c 倒序,从大到小,从右往左。
对于 将二维dp转化为一维dp的问题,其实就是利用一维数组代替二维数组,利用一维数组不断更新,而且更新需要保证不能将后续需要的数值覆盖。
以下分两类情况讨论。
这类问题在二维转一维的时候,可以直接丢弃第一个维度 i
即转化为 a[ j ] = a[ j ] 或者 a[ j ] = [ j - 1 ],因为不会出现值覆盖的情况
这类问题有正序和逆序两种赋值方式。
2.1 正序(第二重循环 j )
1)采用临时变量pre。如上图所示,需要额外的临时变量 pre来保存即将被覆盖的数值。这样才能保证 一维的 a[ j ] = a[ j - k ]与原先二维的a[ i ][ j ] = a[ i - 1 ][ j - k ] 等价
2)不采用临时变量。如上图所示,如果直接正序赋值,会导致值覆盖。
一维的 a[ j ] = a[ j - k ]将等价于二维的a[ i ][ j ] = a[ i ][ j - k ] ,而非原先的a[ i ][ j ] = a[ i - 1 ][ j - k ]。
2.2 逆序(第二重循环 j )
逆序的前提是,二维数组的第 0 行,或者说一维数组的全部值的初始值都是已知的,这样从后往前就不会导致值的覆盖。这种思想在数组排序中有所体现,从后往前插入的话需要挪动的位置更少。
我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。有的题目要求"恰好装满背包"时的最优解,有的题目则并没有要求必须把背包装满。这两种问法的区别是在初始化的时候有所不同。
如果是第一种问法,要求恰好装满背包,那么在初始化时除了 f [ 0 ] 为0,其它 f [1…V] 均设为−inf,这样就可以保证最终得到的 f [ N ] 是一种恰好装满背包的最优解。
如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将
f [ 0…V] 全部设为0。
为什么呢?可以这样理解:初始化的 f 数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为 0 的背包可能被价值为 0 的 nothing “恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是−inf 了。即:
1). f [ 0 ] == 背包容量 0 (合法)
2). f [ 1, …, V] == -inf < 背包容量 0 (不合法)
如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为 0 了。即:
1). f [ 0 ] == 背包容量 0 (合法)
2). f [ 1, …, V] == 0 == 背包容量 0 (合法)
这个小技巧完全可以推广到其它类型的背包问题,后面也就不再对进行状态转移之前的初始化进行讲解。