📝个人主页:@Sherry的成长之路
🏠学习社区:Sherry的成长之路(个人社区)
📖专栏链接:练题
🎯长路漫漫浩浩,万事皆有期待
文章目录
- 零钱兑换 II
- 组合总和 Ⅳ
- 总结:
这两道题是对于完全背包题型的另一个维度,都是求解给定背包容量求装满背包最多有几种方法的题目。两道题十分相像,但在遍历顺序上却又有着极其微妙的差别。
零钱兑换 II
518. 零钱兑换 II - 力扣(LeetCode)
零钱兑换II是完全背包中的装满背包有几种方法的题,而我们以前做过的目标和很是相像,那个需要限定物品只有一个而这道题的区别仅仅是不限定物品的数量。
dp数组的含义:还是根据动规的五部曲来分析,首先dp数组的含义一定是容量为j时候,可以有多少种方法填满背包。
递推公式:递推公式与目标和那道题是一样的。dp【j】+=dp【j-nums【i】】虽然那期已经讲过了,递推公式是如何得到了,但是在本期我想对此再作进一步的补充说明。我们还是以j=5来举例,实际上可以将该递推公式想像成dp【5】=dp【5-nums【0】】+dp【5-nums【1】】+dp【5-nums【2】】+…这样展开来看是不是就更容易理解了,就是先假定我们一定会加入该物品,然后我们再看背包容量j减掉当前物品的重量后,还有几种方法。因为dp【j】所代表的就是当前容量几种方法,所以减掉物品重量后,我们可以用到之前求得的dp来累加起来。背包容量每扩大一点,所对应的填满方法个数,都需要用它之前的背包容量的方法来累加计算得到。
dp数组的初始化:我们之前就说果dp【0】=1,这是因为我们累加如果第一个数就是0,那么加到最后也还是0,其他非0的部分初始化为0,这是因为递推公式的缘故,是累加起来的,如果非0部分初始化不为0,累加时候会不准。换句话来说,累加取决的应该是当前容量之前的各个容量和,而并非受当前容量本身所影响。
遍历顺序:遍历顺序由于是完全背包模型的缘故,需要将背包正序遍历,以装入重复物品。至于是先遍历背包还是先遍历物品也是有讲究的。这道题我们通过测试用例可以看得出来,很明显测试用例都是组合,而非排列,也就是说我们求得的是无顺序的,{1,2}和{2,1}应该看成一种情况!我们这个时候应该先遍历物品后遍历背包,先锁定物品再装入背包这样的好处是,如果数组是{1,2}那么2一定会出现在1的后面被添加,而不是出现在前面,因为先遍历物品的缘故。如果是相反的先遍历背包,那么很有可能会在背包里同时出现{1,2}和{2,1}的情况。我认为这与递推公式也有一定的关系,如果是先遍历背包的话,那么锁定背包容量去遍历物品,当前的循环就可能出现以下情况,当背包容量为3,物品为{1,2},遍历到第一个物品,背包装物品1后根据递推公式是再加等于dp【3-1】,遍历到第2个物品时候,根据递推公式是再加等于dp【3-2】这就造成了我上面说的重复添加,也就是排列的情况了!这相当于二维数组中的同一层在一次填数时候,连续更新了背包!如果是先遍历物品再遍历背包的情况,那么当前里层循环只有装入当前遍历的物品和不装入两种情况,当前并不能加入其它物品,也就不会出现排列的情况,那有人说那你第二次加入物品呢?比如第一次加入1,第二次加入2呢?那不是和之前一样了吗?其实不然,因为第二次加入物品2,那在二维数组中相当于下一层填数了,并不影响上一层,别忘了一维数组是滚动数组。一个是同一层的连续更新,一个是一层一层的更新,是有区别的。
这是我对遍历顺序的理解,我们也可以简记为:
求组合,先遍历物品,求排列,先遍历背包
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=0;j<=amount;j++){
if(j>=coins[i])
dp[j]+=dp[j-coins[i]];
}
}
return dp[amount];
}
};
组合总和 Ⅳ
377. 组合总和 Ⅳ - 力扣(LeetCode)
这道题实际上的思路和上一道题完全一样,只不过这道题是求排列的。这里就不进行动规分析了!
我们只需要将遍历顺序改成我上面说的规律,使for循环的两层上下颠倒即可。
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++){
if(j>=nums[i]&&dp[j]<INT_MAX-dp[j-nums[i]])
dp[j]+=dp[j-nums[i]];
}
}
return dp[target];
}
};
仍有一点需要注意,就是我们在判断部分还要写上dp[j]<INT_MAX-dp[j-nums[i]],这是因为测试用例中有出现两个数相加超过int承载范围的数字。但是我们不能直接写成dp【j】+dp【j-nums【i】】<INT_MAX,因为两个数相加太大它也会报错,所以我们要将一个数转移到等号右边,使它做差,这样就可以避免报错了!
总结:
今天我们完成了零钱兑换 II、组合总和 Ⅳ两道题,相关的思想需要多复习回顾。接下来,我们继续进行算法练习。希望我的文章和讲解能对大家的学习提供一些帮助。
当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~