完全背包问题并记录选择物品和个数(python实现)
现有n种物品(每种物品有任意多个),每个物品的重量为w,对应的价值是v。同时有一背包总载重为W,对于这个背包挑选那几样物品可使其价值总和最大。
在01背包问题中,每种物品只有选择和不选择两种,而在完全背包问题中,每种物品的选择就有很多种,即0,1,…
⌊
W
/
w
i
⌋
\left\lfloor{W}/{w_i}\right\rfloor
⌊W/wi⌋种。由01背包问题可只递推关系如下:
d
p
[
i
]
[
j
]
dp\left[i\right]\left[j\right]
dp[i][j] 表示在还有0~i种物品中可选择,剩余的背包空间为j的情况下背包所能取得的最大价值。
1 转化为01背包问题
将完全背包问题转化为01背包问题:将完全背包问题中的每种物品的多种选择转化为每种物品的每一种选择都作为01背包问题中的一个物品。具体程序如下:
import numpy as np
w = [0,4,1,3,2]#每种物品的重量
v = [0,3,2,4,3]#每种物品的价值
W = 10#背包总载重
n = 4#物品种类
def comlete_0(n,W):
dp = np.zeros((n+1,W+1))
for i in range(1,n+1,1):
for j in range(W+1):
if w[i] == 0:
break
for k in range(int(j/w[i])+1):
dp[i][j] = max(dp[i][j],dp[i-1][j-k*w[i]]+k*v[i])
return dp
if __name__ == "__main__":
print(comlete_0(n,W))
程序的输出结果为:
[[ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 3. 3. 3. 3. 6. 6. 6.]
[ 0. 2. 4. 6. 8. 10. 12. 14. 16. 18. 20.]
[ 0. 2. 4. 6. 8. 10. 12. 14. 16. 18. 20.]
[ 0. 2. 4. 6. 8. 10. 12. 14. 16. 18. 20.]]
但是把完全背包问题转化为01背包问题,时间和空间复杂度就很大,时间复杂度最坏的情况是O( n W 2 nW^2 nW2),可以使用动态规划对该算法进行优化。
2 动态规划方法
首先得出完全背包问题的递推公式如下:。
d p [ i ] [ j ] = max ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − w i ] + v i ) dp\left[i\right]\left[j\right]=\max(dp[i-1][j],dp[i][j-w_i]+v_i) dp[i][j]=max(dp[i−1][j],dp[i][j−wi]+vi)
接下来我们看看如何理解这个公式:
- 当k=0的情况下,当前i种物品没有被选择,即 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j];
- 当 k ≠ 0 k\neq0 k=0的情况下,对当前i种物品进行了选择,至少选择了i种物品的一个物品,即 d p [ i ] [ j − w i ] + v i dp[i][j-w_i]+v_i dp[i][j−wi]+vi。
- d p [ i ] [ j − w i ] dp[i][j-w_i] dp[i][j−wi]表达的含义是在还有0~i种物品中可选择,剩余的背包空间为 j − w i j-w_i j−wi的情况下背包所能取得的最大价值。而 d p [ i ] [ j − w i ] dp[i][j-w_i] dp[i][j−wi]本身又需要通过 d p [ i ] [ j − k × w i ] , 2 ≤ k ≤ ⌊ j / w i ⌋ − 1 dp\left[i\right]\left[j-{k\times w}_i\right],2≤k≤\left\lfloor{j}/{w_i}\right\rfloor-1 dp[i][j−k×wi],2≤k≤⌊j/wi⌋−1求得,即 d p [ i ] [ j − w i ] dp[i][j-w_i] dp[i][j−wi]值中又包含 d p [ i ] [ j − 2 × w i ] dp\left[i\right]\left[j-{2\times w}_i\right] dp[i][j−2×wi],而 p [ i ] [ j − 2 × w i ] p[i][j-2×wi] p[i][j−2×wi]本身又需要通过 d p [ i ] [ j − k × w i ] , 3 ≤ k ≤ ⌊ j / w i ⌋ − 2 dp\left[i\right]\left[j-{k\times w}_i\right],3≤k≤\left\lfloor{j}/{w_i}\right\rfloor-2 dp[i][j−k×wi],3≤k≤⌊j/wi⌋−2求得…,我们发现求解 d p [ i ] [ j − w i ] dp[i][j-w_i] dp[i][j−wi]本身的过程就包含了当前i种物品除了不选择之外的所有选择组合了。
- 而 max ( d p [ i − 1 ] [ j − k × w i ] + k × v i ) , 1 ≤ k ≤ ⌊ j / w i ⌋ \max{\left(dp\left[i-1\right]\left[j-k\times w_i\right]+k\times v_i\right)},1≤k≤\left\lfloor{j}/{w_i}\right\rfloor max(dp[i−1][j−k×wi]+k×vi),1≤k≤⌊j/wi⌋这个公式中 d p [ i − 1 ] [ j − k × w i ] dp\left[i-1\right]\left[j-k\times w_i\right] dp[i−1][j−k×wi]所要表达的含义是在还有0~(i-1)种物品中可选择,剩余的背包空间为 j − k × w i j-k\times w_i j−k×wi的情况下背包所能取得的最大价值,k的取值是 1 ≤ k ≤ ⌊ j / w i ⌋ 1\le k\le\left\lfloor{j}/{w_i}\right\rfloor 1≤k≤⌊j/wi⌋,也是当前物品i必须选择下的所有选择组合。
所以可以看出 max ( d p [ i ] [ j − w i ] + v i ) \max(dp[i][j-wi]+vi) max(dp[i][j−wi]+vi)和 max ( d p [ i − 1 ] [ j − k × w i ] + k × v i ) \max{\left(dp\left[i-1\right]\left[j-k\times w_i\right]+k\times v_i\right)} max(dp[i−1][j−k×wi]+k×vi)所表达的含义相同。
根据状态转移方程:
d p [ i ] [ j ] = max ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − w i ] + v i ) dp\left[i\right]\left[j\right]=\max(dp[i-1][j],dp[i][j-w_i]+v_i) dp[i][j]=max(dp[i−1][j],dp[i][j−wi]+vi)
代码实现如下:
import numpy as np
w = [0,4,1,3,2]#每种物品的重量
v = [0,3,2,4,3]#每种物品的价值
W = 10#背包总载重
n = 4#物品种类
def comlete_1(n,W):
dp = np.zeros((n+1,W+1))
for i in range(1,n+1,1):
for j in range(W+1):
if j < w[i]:
dp[i][j] = dp[i-1][j]
else:
dp[i][j] = max(dp[i-1][j],dp[i][j-w[i]]+v[i])
return dp
if __name__ == "__main__":
print(comlete_1(n,W))
程序的输出结果为:
[[ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 3. 3. 3. 3. 6. 6. 6.]
[ 0. 2. 4. 6. 8. 10. 12. 14. 16. 18. 20.]
[ 0. 2. 4. 6. 8. 10. 12. 14. 16. 18. 20.]
[ 0. 2. 4. 6. 8. 10. 12. 14. 16. 18. 20.]]
3 动态规划方法优化
通过动态规划,时间复杂度减少到了O(nW),空间复杂度没有改变。根据状态转移方程可知, d p [ i ] [ j ] dp[i][j] dp[i][j]依赖于 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j]和 d p [ i ] [ j − w [ i ] ] dp[i][j-w[i]] dp[i][j−w[i]],也就是 d p [ i ] [ j ] dp[i][j] dp[i][j]依赖于第i-1行和第i行,可以转化为2行,最总合并成1行,如下所示:
如表格显示右下角的20是由当前行的16和上一行的20(红圈)计算得到的。可以将状态转移方程变化为:
d p [ j ] = max ( d p [ j ] , d p [ j − w i ] + v i ) dp\left[j\right]=\max(dp[j],dp[j-w_i]+v_i) dp[j]=max(dp[j],dp[j−wi]+vi)
具体程序如下:
import numpy as np
w = [0,4,1,3,2]#每种物品的重量
v = [0,3,2,4,3]#每种物品的价值
W = 10#背包总载重
n = 4#物品种类
def comlete_2(n,W):
dp = np.zeros((W+1,))
for i in range(1,n+1,1):
for j in range(W+1):
if j < w[i]:
dp[j] = dp[j]
else:
dp[j] = max(dp[j],dp[j-w[i]]+v[i])
return dp
if __name__ == "__main__":
print(comlete_2(n,W))
程序执行结果:
[ 0. 2. 4. 6. 8. 10. 12. 14. 16. 18. 20.]
4 获取完全背包选择物品的种类和数量
首先要获取完全背包选择物品的种类和数量,必须使用完全背包问题动态规划未优化版本(即上面‘2 动态规划方法’),因为只有‘2 动态规划方法’能够获取状态转移的完整矩阵,通过状态转移矩阵反推出选择物品的种类和数量,因此必须理解完全背包问题的状态转移方程:
d p [ i ] [ j ] = max ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − w i ] + v i ) dp\left[i\right]\left[j\right]=\max(dp[i-1][j],dp[i][j-w_i]+v_i) dp[i][j]=max(dp[i−1][j],dp[i][j−wi]+vi)
w = [0,4,2,3,2]#每种物品的重量
v = [0,4,1,5,3]#每种物品的价值
W = 10#背包总载重
n = 4#物品种类
在上述情况下的状态转移方程如下图:
d p [ i ] [ j ] dp[i][j] dp[i][j]依赖于 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j]和 d p [ i ] [ j − w [ i ] ] dp[i][j-w[i]] dp[i][j−w[i]],也就是 d p [ i ] [ j ] dp[i][j] dp[i][j]依赖于第i-1行和第i行,如表格显示右下角的16是由同行的13和上一行的15(红圈)计算得到的,动态规划是自下而上的,叫求选择的物品种类和数量必须自上而下求,从右下角16开始计算,图中右下角的16由由同行的13(最后一种物品的重量是2即图中向左移动两格,根据状态转移方程可知是图中同行的13)和上一行的15,16不等于上一行的15即 d p [ i − 1 ] [ j ] < d p [ i ] [ j − w i ] + v i dp[i-1][j]<dp[i][j-w_i]+v_i dp[i−1][j]<dp[i][j−wi]+vi,所以最后一种物品被选择了一次,依此从状态图中向上搜索就可以得到选择的物品种类和数量。
代码实现如下:
import numpy as np
w = [0,4,2,3,2]#每种物品的重量
v = [0,4,1,5,3]#每种物品的价值
W = 10#背包总载重
n = 4#物品种类
def comlete_1(n,W):
dp = np.zeros((n+1,W+1))
for i in range(1,n+1,1):
for j in range(W+1):
if j < w[i]:
dp[i][j] = dp[i-1][j]
else:
dp[i][j] = max(dp[i-1][j],dp[i][j-w[i]]+v[i])
j=W
for i in range(n,0,-1):
while (j-w[i]) >=0:
if ((dp[i][j-w[i]]+v[i])>dp[i-1][j]):
j-=w[i]
print('i=',i)#记录选择的物品
else:
break
return dp
if __name__ == "__main__":
print(comlete_1(n,W))
程序的输出结果为:
i= 4
i= 4
i= 3
i= 3
[[ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 4. 4. 4. 4. 8. 8. 8.]
[ 0. 0. 1. 1. 4. 4. 5. 5. 8. 8. 9.]
[ 0. 0. 1. 5. 5. 6. 10. 10. 11. 15. 15.]
[ 0. 0. 3. 5. 6. 8. 10. 11. 13. 15. 16.]]
可以看到i=3有两次,i=4有两次,也就是第三种物品被选择了两次,第四种物品被选择了两次。