ps:题目来源于leetcode
排列 vs组合
组合总和 Ⅳ VS 零钱兑换 II
首先这一对非常有代表性,引出了两个非常常见且易搞混的概念,排列与组合。
我们假设组合是一组数的集合,但是该集合中数的顺序不做要求,而排列即使集合中的数相同但排序不同就算不同的排列(排列是需要考虑顺序的)。
在其他方面可以说这两题一模一样,可以类比,比如目标整数类比于总金额,不同整数组成的数组类比于不同面值的硬币。用数组凑目标整数可以理解为用硬币凑金额。
组合总和 Ⅳ
int combinationSum4(vector<int>& nums, int target) {
int n=nums.size();
vector<int> dp(target+1, 0);
dp[0]=1;
for(int i=1;i<=target;i++){
for(auto& num:nums){
if(i>=num && dp[i-num]<INT_MAX-dp[i]){
dp[i]+=dp[i-num];
}
}
}
return dp[target];
}
由于排列要考虑顺序,所以选择的数组在内层循环。
零钱兑换 II
int change(int amount, vector<int>& coins) {
vector<int> dp(amount+1, 0);
//初始化
//当不选取任何硬币时,金额之和才为0,因此只有 1种硬币组合。
dp[0]=1;
int n=coins.size();
for(auto& coin:coins){
for(int j=coin;j<=amount;j++){
dp[j]+=dp[j-coin];
}
}
return dp[amount];
}
由于是组合不考虑顺序,所以数组放在外层循环。
不同点:
其实零钱兑换 II属于完全背包问题,把零钱数组类比于物品价值或体积数组,所以放在外层循环是比较好理解的。现在就是着重分析为什么组合总和 Ⅳ是放在内层循环。
这里我们需要引申一题基础题爬楼梯
int climbStairs(int n) {
vector<int> dp(n+1,0);
//初始化dp
dp[0]=1,dp[1]=1;
for(int i=2;i<=n;i++){
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
题解如上:该题dp[i]表示爬上i层楼梯的方法数,由于一次只能一步或者两步,所以状态转移方程为dp[i]=dp[i-1]+dp[i-2]。
那么我仔细分析一下,这题爬的楼梯层数可以类比为组合总和 Ⅳ的目标整数target,一步或者两步可以类比为组合总和 Ⅳ的由1和2组成的数组[1, 2]。并且这题爬楼的方式数也考虑顺序,比如你先走一步再走两步明显和先走两步再走一步不一样,所以说和组合总和 Ⅳ这题本质一样。
如果还觉得不像看看我下面的类比写法
组合总和 Ⅳ:dp[i]+=dp[i-num]
爬楼梯:dp[i]=dp[i-1]+dp[i-2] => dp[i]+=dp[i-1] dp[i]+=dp[i-2]
这两状态转移方程一模一样,唯一不同就是组合总和 Ⅳ给的数组nums不是固定的,而爬楼梯相当于给了你固定只有1和2的nums数组。比如我改造代码如下书写:
int climbStairs(int n) {
vector<int> dp(n+1,0);
//包含1与2的nums数组
vector<int> nums{1,2};
//初始化dp
dp[0]=1;
for(int i=1;i<=n;i++){
for(auto& num:nums){
if(i>=num){
dp[i]+=dp[i-num];
}
}
}
return dp[n];
}
我这样写之后,发现组合总和 Ⅳ与爬楼梯这题代码不能说很像,只能说一模一样。一个是题目给的可变nums,另一个是题目固定死的nums。
所以说nums在内循环,本质只是计算
dp[i]=dp[i-nums[0]]+dp[i-nums[1]]+dp[i-nums[2]]+dp[i-nums[3]]+······罢了,没有什么特别之处。
类比爬楼梯的计算dp[i]=dp[i-1]+dp[i-2]。