第一部分 完全背包理论基础
完全背包问题就是给定背包的容量,并给出一组物品的重量与价值,问这个背包中能装的最大价值是多少,并且每个物品可以使用无限次。与01背包的区别就在于:01背包问题中,每个物品最多只能用一次,而完全背包问题中的背包可以用无数次。
完全背包是要求从前向后遍历的,这与01背包也是不同的,对于一维数组求01背包问题,应该从后向前遍历,这是因为每个物品只能被放入背包一次,从前向后会导致重复放入(因为递推公式依赖于上一行左侧数据);而完全背包必须从前向后,它的递推公式依赖于这一行的左侧数据。对于纯完全背包问题而言,先遍历物品还是先遍历背包效果是一样的,都能得到正确结果。但是,对于某些题目而言,遍历顺序就变得很关键了!下面两道题目就会说明这个情况。
第一题 518. 零钱兑换 II
这道题是一个完全背包中的组合问题,也就是说元素的顺序不会有影响(例如:【1,1,2】与【2,1,1】这两中形式等同)。
按照动归五部曲来解题:
1.确定dp数组下标含义:用dp[j]来表示凑出总金额为j的方式数量;
2.确定递推公式:对于组合问题,公式为:dp[j] += dp[j - nums[i]];
3.初始化dp数组:令dp[0] = 1,其余为0;
4.确定遍历顺序:对于完全背包中的组合问题,应当先遍历物品,后遍历背包容量;并且,要从前向后依次遍历更新数组。
5.打印dp数组:有问题的话就打印dp数组来debug。
根据上述步骤,可以得出如下代码:
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];
}
};
第二题 377. 组合总和 Ⅳ
这道题很明显是一个排列问题,因为题目告诉我们,(1, 1, 2) 和(1, 2, 1)是两个不同的结果。与此同时,可以发现这是一个完全背包问题,背包容量为target,求刚好放满背包的情况有多少种?与上一题非常相似,唯一的不同就在于本题是排列。对于排列,我们应该先遍历背包容量,后遍历物品。
按照动归五部曲写出代码如下:
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<int> dp(target + 1);
dp[0] = 1;
for(int j = 0; j <= target; j++){ //先遍历背包容量
for(int i = 0; i < nums.size(); i++) { //后遍历物品
if(j >= nums[i] && dp[j] < INT_MAX - dp[j - nums[i]])
dp[j] += dp[j - nums[i]];
}
}
return dp[target];
}
};
总结
今天学习了完全背包相关知识,可以发现,对排列和组合不同要求的题目,遍历顺序极其关键。Day44打卡!