参考
题目
这次不求凑成总金额所需的最少硬币个数,而是求组合总数。
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 1:
输入: amount = 5, coins = [1, 2, 5]
输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
示例 2:
输入: amount = 3, coins = [2]
输出: 0
解释: 只用面额2的硬币不能凑成总金额3。
示例 3:
输入: amount = 10, coins = [10]
输出: 1
注意:
你可以假设:
0 <= amount (总金额) <= 5000
1 <= coin (硬币面额) <= 5000
硬币种类不超过 500 种
结果符合 32 位符号整数
思路:动态规划,注意是组合,不是排列
此时内外循环可换。
class Solution {
public:
/*
动态规划。设状态方程dp[i]表示凑成总金额i的组合数,我们要求的是dp[amount]。初始状态dp[0]=1,只有有人到了这步说明刚好满足所以+1
状态转移方程:对于每一种硬币coins[j],如果不放那组合数没变化;如果放了,那么组合数有dp[i-coins[j]]。因此dp[i] = dp[i] + dp[i-coins[j]]
但是这个有问题!我们会把1,2和2,1当成两成情况处理,计算出来的数是排列数而不是组合数!
int change(int amount, vector<int>& coins) {
if( amount<=0 || coins.size()==0 ){
return 0;
}
vector<int> dp(amount+1, 0); //dp初始化为0
// for(int i=0; i<=amount; i++) dp[i] = 0;
dp[0] = 1;
for(int i=1; i<=amount; i++){
for(int j=0; j<coins.size(); j++){
if( i >= coins[j] ){
dp[i] = dp[i] + dp[i-coins[j]];
}
}
}
return dp[amount];
}
*/
/*
我们的是求组合数,而不是排列数,所以无关顺序。思路可以理解成哪几个空用来放硬币a,哪几个空用来放硬币b。也就是对每一种硬币coins[k],我们都遍历一遍dp[k,0...amount],dp[k,i]表示第0..k种硬币能凑i的组合数,即前k+1种硬币能凑i的组合数。
动态规划:dp[k,i]表示在coins[0...k-1]中选取某类硬币可以凑成总金额i的组合数,我们最终要求的是dp[coins.size(), amount]。注意dp都扩容了1,这是为了初始化以及最终求解。
状态初始化dp[..][..]=0,但是dp[..,0]=1。
状态转移方程:当前考虑硬币为coins[k],要么放,要么不放。如果不放,那就是上一次已经凑足了,即dp[k-1,i]。如果放的话,那么就是之前已经凑足i-coins[k]的钱,只差现在这一个了,即dp[k, i-coins[k]],注意不是dp[k-1, i-coins[k]]!!两种情况加起来,dp[k,i] = dp[k-1,i] + dp[k, i-coins[k]]
然后由于每一次都只跟上一次状态相关,所以我们可以考虑降维优化.
*/
int change(int amount, vector<int>& coins) {
if( amount==0 ){ //默认金额就是0,所以存在一种组合数,那就是谁都不放
return 1;
}
if( coins.size()==0 ){ //没有硬币
return 0;
}
int dp[coins.size()+1][amount+1];
//数组初始化
for(int k=0; k<=coins.size(); k++){
for(int i=0; i<=amount; i++){
dp[k][i] = 0;
}
}
//状态初始化:到达金额0的都是符合要求
for(int k=0; k<=coins.size(); k++){
dp[k][0] = 1;
}
//从coins[0...k-1]中自由选取硬币,凑成总金额i。所以两层循环
for(int k=1; k<=coins.size(); k++){
//当前硬币 coins[k-1]
for(int i=1; i<=amount; i++){
if( i >= coins[k-1] ){
//当前硬币选择:不放 or 放
dp[k][i] = dp[k-1][i] + dp[k][i-coins[k-1]]; //注意判断i-coins[k]
}else{
dp[k][i] = dp[k-1][i];
}
}
}
return dp[coins.size()][amount];
}
};
降维优化
此时dp[i]表示第k个硬币能凑的组合数,内外循环不可换。
class Solution {
public:
/*
我们的是求组合数,而不是排列数,所以无关顺序。思路可以理解成哪几个空用来放硬币a,哪几个空用来放硬币b。也就是对每一种硬币coins[k],我们都遍历一遍dp[k,0...amount],dp[k,i]表示第0..k种硬币能凑i的组合数,即前k+1种硬币能凑i的组合数。
动态规划:dp[k,i]表示在coins[0...k-1]中选取硬币可以凑成总金额i的组合数,我们最终要求的是dp[coins.size(), amount]。注意dp都扩容了1,这是为了初始化以及最终求解。
状态初始化dp[..][..]=0,但是dp[...,0]=1。
状态转移方程:当前考虑硬币为coins[k],要么放,要么不放。如果不放,那就是上一次已经凑足了,即dp[k-1,i]。如果放的话,那么就是之前已经凑足i-coins[k]的钱,只差现在这一个了,即dp[k, i-coins[k]],注意不是dp[k-1, i-coins[k]]!!两种情况加起来,dp[k,i] = dp[k-1,i] + dp[k, i-coins[k]]
然后由于每一次都只跟上一次状态相关,所以我们可以降维优化
*/
int change(int amount, vector<int>& coins) {
if( amount==0 ){ //默认金额就是0,所以存在一种组合数,那就是谁都不放
return 1;
}
if( coins.size()==0 ){ //没有硬币
return 0;
}
//dp[i]表示在coins[0...k-1]中选取硬币可以凑成总金额i的组合数
int dp[amount+1];
//数组初始化
for(int i=0; i<=amount; i++){
dp[i] = 0;
}
//状态初始化:到达金额0的都是符合要求、
dp[0] = 1;
//从coins[0...k-1]中自由选取硬币,凑成总金额i。所以两层循环
for(int k=1; k<=coins.size(); k++){
//当前硬币 coins[k-1]
for(int i=1; i<=amount; i++){
if( i >= coins[k-1] ){
//当前硬币选择:不放 or 放
dp[i] = dp[i] + dp[i-coins[k-1]]; //注意判断i-coins[k]
}else{
dp[i] = dp[i];
}
}
}
return dp[amount];
}
};