背包九讲
0-1背包
题目
有N件物品和最多承载量W的背包。第i件物品的重量是wi,其价值是vi。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
基本思路
作为最基础的背包问题,其特点是每件物品最多用一次
用子问题定义状态F[i,w]表示前i件物品放入承载量w的背包可获得的最大价值,则状态转移方程便是
F[i,w]=max(F[i−1,w],F[i−1,w−wi]+vi)
其代表的意思是若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只和前i−1件物品相关的问题。
- 如果不放第i件物品,问题就转化为“前i−1件物品放入容量为w的背包中”,价值为F[i−1,w];
- 如果放第i件物品,问题就转化为“前i−1件物品放入剩下的容量为w−wi的背包中”,此时能获得的最大价值就是F[i−1,w−wi]再加上通过放入第i件物品获得的价值 wi。
代码实现
先遍历物品再遍历容量(横向更新)
先遍历容量再遍历物品(纵向更新)
蓝色方块表示更新完成,绿色表示当前需要更新的值,红色表示还未更新,带文字的方块表示更新当前值所需要的值。
可以看出,对于二维实现,先遍历背包容量或者先遍历物品都可以,因为更新当前位置所需信息时所需要的值(蓝色带文字方块)都已经更新完成
bagWeight为背包重量,weight表示重量数组,value表示价值数组,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| int zeroOnePack(int bagWeight,vector<int> weight,vector<int> value){ int m = weight.size(), n = bagWeight; int dp[m+1][n+1]; for(int i = 0; i < m+1; i++) dp[i][0] = 0; for(int i = 0; i < n+1; i++) dp[0][i] = 0;
for(int i = 1; i < weight.size(); i++) { for(int j = 1; j <= bagWeight; j++) { if (j < weight[i]) dp[i][j] = dp[i - 1][j]; else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); } } return dp[m][n]; }
|
1 2 3 4 5 6 7 8 9 10
| for(int j = 1; j <= bagWeight; j++) { for(int i = 1; i < weight.size(); i++) { if (j < weight[i]) dp[i][j] = dp[i - 1][j]; else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); } }
|
空间优化
采用一维数组去替代二维数组,这要求在dp能够同时保存上一层信息和更新本层
因为更新当前位置所需信息只在上一层左边和正上方,必需采用逆序。
1 2 3 4 5 6 7 8 9 10 11 12 13
| int zeroOnePack(int bagWeight,vector<int> weight,vector<int> value){ vector<int> dp(bagWeight+1,0); for(int i = 0; i < weight.size(); i++) {
for(int j = bagWeight; j >= weight[i]; j--) { dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); } } return dp[bagWeight];
}
|
完全背包
完全背包与0-1背包的区别在于物品的个数是无限的,即可重复选择相同物品填充
基本思路
- 基本思路
模仿0-1背包的解法
F[i,w]=max{F[i−1,w−k∗wi]+k∗vi∣0<=k∗wi<=W}
- 转换为0-1背包
每个物品i,最多能放wiW件,那么就把第i件扩充至wiW件,然后解决这个0-1背包问题
则状态转移方程便是F[i,w]=max(F[i−1,w],F[i,w−wi]+vi)
- 如果不放第i件物品,问题就转化为“前i−1件物品放入容量为w的背包中”,价值为F[i−1,w];
- 在考虑“选一件第i种物品”时,需要一个可能已选入第i种物品的子结果F[i,w−wi]。而不是只选到前i−1的子结果F[i−1,w−wi]
二维实现
先遍历物品再遍历容量(横向更新)
先遍历容量再遍历物品(纵向更新)
和0-1背包一样,对于二维实现,先遍历物品和先遍历容量都是可行的。原因就在于不管采用那种遍历顺利总是保证更新当前位置时所需要的信息已经准备好了
bagWeight为背包重量,weight表示重量数组,value表示价值数组,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| int completePack(int bagWeight,vector<int> weight,vector<int> value){ int m = weight.size(), n = bagWeight; int dp[m+1][n+1]; for(int i = 0; i < m+1; i++) dp[i][0] = 0; for(int i = 0; i < n+1; i++) dp[0][i] = 0;
for(int i = 1; i < weight.size(); i++) { for(int j = 1; j <= bagWeight; j++) { if (j < weight[i]) dp[i][j] = dp[i - 1][j]; else dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]); } } return dp[m][n]; }
|
空间优化
采用一维数组去替代二维数组,
因为更新当前位置所需信息本层左边和正上方,对应到一维就是当前位置左边和自身位置,需采用正向遍历,及时更新本层左边方块
1 2 3 4 5 6 7 8 9
| int completePack(int bagWeight,vector<int> weight,vector<int> value){ vector<int> dp(bagWeight+1,0); for(int i = 0; i < weight.size(); i++) { for(int j = weight[i]; j <= bagWeight; j++) { dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); } } return dp[bagWeight]; }
|
总结
- 不管是0-1背包还是完全背包,其二维实现两种遍历顺序都是可以的
- 0-1背包和完全背包解决的是组合问题,并非排列问题,排列问题参考爬楼梯
- 0-1背包和完全背包组合问题都是外层遍历物品,内层遍历背包容量。
- 0-1背包是逆向遍历背包容量,保证当前物品最多一个被加入。完全背包正向遍历,使得可以当前物品可多次被加入
- 对于排列问题,先遍历背包,在遍历物品。参考爬楼梯进阶版