● 完全背包
完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。一个物品可以无数次放入背包。
仍然是学习01背包的例子:
①用一维滚动数组的话,dp[j]含义、初始化、递推公式都是不变的,因为这些跟物品的数量是无关的。所以解决01背包和完全背包,方法的区别就是遍历顺序,又可见背包问题中遍历顺序的重要性。
回想01背包,是先物品后背包,而且背包逆序遍历。递推公式:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
01背包逆序是为了不重复放入某个物品,因为dp[j]依靠物品 i 这一行左边的值得来,从左往右顺序来的话左边某个值放入了这一行代表的物品i,dp[j]取的是上面递推公式的右边的值,那就会再加一次value[i],也就是又放入一次物品 i。
所以从左往右不符合01背包要求,但是却是符合完全背包的要求的 ,因为物品i有无限多个,对于物品i这行,想放几次就放几次,那么肯定从左往右才能得到完全背包的最大值。完全背包的最大值肯定是比01背包的最大值要大,因为完全背包较大的一些物品的价值可以重复放入,总的价值就会增大,所以要想最大,就得采用从左往右这种可以重复取物品的顺序。
另外,纯完全背包先物品或者先背包是都可以的。我们回忆为什么01背包只能先物品再背包,首先01背包只能逆序遍历背包(下图顺序④),然后只能先物品再背包,所以采用的顺序只能是顺序④,再顺序①(也就是先从右到左,再从上到下),如果先①再④,在第一轮外层循环(最后一列)就确定了dp[2][4],这样肯定是不对的。
我们再看完全背包,因为从左到右(顺序②),所以两种选择:先①再②(先列);先②再①(先行)。这两种其实都是可以的,因为完全背包不用避免覆盖,所以这两种选择都会把节点这一行左边的dp都更新,吻合我们想要重复选择物品的意愿。所以两个选择区别就是节点左下的节点是先于自己更新还是后于自己更新,这个不会影响最后的结果,因为每轮都取最大值,所以两种选择最后表格内的结果都是一样的。
先①再②:
先②再①:
还可以把物品顺序打乱来检验(如下表,1、2、3行对应物品2、物品1、物品0),发现dp[2][4]是一样的,都是60,只是其他的中间值不同:
0 | 0 | 0 | 0 | 30 |
0 | 0 | 0 | 20 | 30 |
0 | 15 | 30 | 45 | 60 |
练习一个纯完全背包题目:卡玛网52题:52题
采用一维dp数组,代码如下:
#include<vector>
#include<iostream>
using namespace std;
void maxValue(){
int N,V;
cin>>N>>V;
vector<int> dp(V+1,0);
for(int i=0;i<N;++i){ //先j后i也行
int wi,vi;
cin>>wi>>vi;
for(int j=wi;j<=V;++j){ //背包是正序
dp[j]=max(dp[j],dp[j-wi]+vi);
}
}
cout << dp[V] << endl;
}
int main(){
maxValue();
return 0;
}
● 518. 零钱兑换 II
- 输入: amount = 5, coins = [1, 2, 5]
- 输出: 4
背包容量给出:amount,物品重量=物品价值:coins数组。
是组合问题,结合之前分析的组合问题和完全背包,开始动规五部曲:
1.dp[j]:有dp[j]种方式凑成j的总金额。与昨天● 494. 目标和 一样。
2.递推:dp[j]+=dp[j-coins[i]]。与昨天● 494. 目标和 一样。
3.遍历顺序:刚刚纯完全背包中说先物品还是先背包都是一样的,但是由于这里的dp[j]含义是求组合数,纯完全背包问题是求最大价值。
那么组合问题的话是先物品还是先背包呢,先物品这种情况,是对于某个物品i,来看逐渐增大的背包是否能装下,所以先在逐渐增大的背包里面看1能不能装下,再看2能不能装下,再看5能不能装下,然后结束。所以只会出现顺序为1,2,5的情况,所以上面输出为4的例子中,4个组合分别是(1,1,1,1,1),(1,1,1,2),(1,2,2),(5),物品相对顺序都是1、2、5,所以只考虑一种排列顺序,求出来的就是组合数。
先背包的情况,是对于某个背包j,来看依次每个物品是否能装下,所以先看背包1能不能装下物品1、2、5,再看背包2能不能装下1、2、5,放入物品的这个顺序是1,2,5,1,2,5,1,2,5……,这里对于这三个数所有的排列(1,2,5;1,5,2;2,1,5;2,5,1;5,1,2,5,2,1)都考虑到了,所以求出来的是排列数而不是组合数。
4.初始化:注意也是跟与昨天● 494. 目标和 一样,dp[0]=1,不能=0,否则结果都是得0。
代码如下:
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int> dp(amount+1,0);
dp[0]=1;
for(int i=0;i<coins.size();++i){
for(int j=coins[i];j<=amount;++j){ //背包是正序遍历
dp[j]+=dp[j-coins[i]];
}
}
return dp[amount];
}
};
5.打印dp数组:
● 377. 组合总和 Ⅳ
注意这道题和上一道的区别,直接使用上一道的源码不改动:
输入:nums = [1,2,3], target = 4
输出的结果是7不是4,根据上面的分析,只需要修改遍历顺序:先背包再物品。
代码如下。注意C++程序的话有相加后超过INT_MAX的情况,需要跳过。
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<int> dp(target+1,0);
dp[0]=1;
for(int j=0;j<=target;++j){
for(int i=0;i<nums.size();++i){
//dp[j]+dp[j-nums[i]]<INT_MAX错误,左边求得超过INT_MAX了就会报错
if(j>=nums[i] && dp[j]<INT_MAX-dp[j-nums[i]]){
dp[j]+=dp[j-nums[i]];
}
}
}
return dp[target];
}
};
5.打印dp数组:
过程中每一列都是相同的数。
总结:所以①②③④的顺序决定了是01背包还是完全背包,是组合问题、or排列问题,or普通的纯背包问题求最大价值。
01背包是④①,纯完全背包是①②或者②①都行,完全背包的组合问题是②①,完全背包的排列问题是①②。