零钱兑换I
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
示例 1:
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
示例 2:
输入: coins = [2], amount = 3
输出: -1
说明:
你可以认为每种硬币的数量是无限的。
带备忘录的递归解法
设dp[n]表示凑成金额j所需要的最少硬币个数
1.base case:
dp[负数]=-1;
dp[0]=0;
2.状态转移
class Solution {
public:
vector<int> memo;
int coinChange(vector<int>& coins,int amount){
if(amount<0) return -1;
if(0==amount) return 0;
//1~amount对应存到memo[0]~[amount-1]
memo.resize(amount);
return dp(coins,amount);
}
int dp(vector<int>& coins,int amount){
if(amount<0) return -1;
if(0==amount) return 0;
if(memo[amount-1]!=0) return memo[amount-1];
int res=INT_MAX;
for(int i=0;i<coins.size();i++){
int pre = dp(coins,amount-coins[i]);
// if(pre!=-1)
// res=min(res,pre+1);
if(pre!=-1&&pre<res)//避免每次调用min(res,pre+1);效率高了不少
res=pre+1;
}
//循环结束后,若res还是INT_MAX,表明无法凑出金额amount
memo[amount-1]= (res==INT_MAX?-1:res);
return memo[amount-1];
}
};
动态规划解法
设dp[n]表示凑成金额j所需要的最少硬币个数
1.base case:
dp[0]=0;
2.状态转移
int coinChange(vector<int>& coins,int amount){
if(amount<0) return -1;
vector<int> dp(amount+1,0);
for(int i=1;i<=amount;i++){
int res=INT_MAX;
for(int j=0;j<coins.size();j++){
if(i>=coins[j]){
int pre=dp[i-coins[j]];
//只有pre!=-1,pre+1才能参与全局最小的比较
if(pre!=-1)
res=min(res,pre+1);
}
}
//res==INT_MAX说明无法凑出i金额
dp[i]= res==INT_MAX?-1:res;
}
return dp[amount];
}
模拟完全背包问题的解法
这里选择了两个状态,
dp[i][j]:表示前i类硬币,凑成金额j元,最少需要的硬币个数。
状态转移
存在:里面存放的是最少硬币个数;
不存在:里面存放的是-1
(1)dp[i-1][j]存在 dp[i][j-coins[i-1]]存在
dp[i][j]= min{dp[i-1][j],dp[i][j-coins[i-1]]+1}
(2)dp[i-1][j]存在 dp[i][j-coins[i-1]]不存在
dp[i][j]= dp[i-1][j]
(3)dp[i-1][j]不存在 dp[i][j-coins[i-1]]存在
dp[i][j]= dp[i][j-coins[i-1]]+1
(4)dp[i-1][j]不存在 dp[i][j-coins[i-1]]不存在
dp[i][j]= -1
base case:
dp[0][1]~dp[0][amount] 都等于-1
dp[][0]=0;
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int N=coins.size();
vector<vector<int>> dp(N+1,vector<int>(amount+1,0));
for(int i=1;i<=amount;i++){
dp[0][i]=-1;
}
for(int i=1;i<=N;i++){
for(int j=1;j<=amount;j++){
//无法放进背包
if(j<coins[i-1])
//注意这里若第i类硬币无法放进背包时,需要继续看第i类前几类是否能凑成金额j
//不能直接dp[i][j]=-1;
dp[i][j]=dp[i-1][j];
//dp[i][j]=-1;
else{
if(dp[i-1][j]!=-1 && dp[i][j-coins[i-1]]!= -1)
dp[i][j]=min(dp[i-1][j],dp[i][j-coins[i-1]]+1);
else if(dp[i-1][j]!=-1 && dp[i][j-coins[i-1]]== -1)
dp[i][j]=dp[i-1][j];
else if(dp[i-1][j]==-1 && dp[i][j-coins[i-1]]!= -1)
dp[i][j]=dp[i][j-coins[i-1]]+1;
else
dp[i][j]=-1;
}
}
}
if(dp[N][amount]==-1)
return -1;
return dp[N][amount];
}
};
虽然通过了,但是效率惨不忍睹
零钱兑换II
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 1:
示例 2:
注意:
你可以假设:
0 <= amount (总金额) <= 5000
1 <= coin (硬币面额) <= 5000
硬币种类不超过 500 种
结果符合 32 位符号整数
直接按照完全背包求解
dp[i][j]:表示一共i类硬币,凑成总金额j的组合数
状态转移:选第i类硬币的组合数+不选第i类硬币的组合数
dp[i][j]= dp[i][j-coins[i-1]] + dp[i-1][j]; 当j>=coins[i-1]时
dp[i][j]= dp[i-1][j]; 当j<coins[i-1]时
base case
dp[0][...]=0
dp[...][0]=1
完整代码
int change(int amount, vector<int>& coins) {
if(amount==0) return 1;
int N=coins.size();
if(N==0) return 0;
vector<vector<int>> dp(N+1,vector<int>(amount+1));
//base case
for(int i=0;i<=N;i++)
dp[i][0]=1;
//状态转移
for(int i=1;i<N+1;i++){
for(int j=1;j<amount+1;j++){
if(j>=coins[i-1])
dp[i][j]=dp[i][j-coins[i-1]]+dp[i-1][j];
else
dp[i][j]=dp[i-1][j];
}
}
return dp[N][amount];
}
官方提供的解法
https://leetcode-cn.com/problems/coin-change-2/solution/ling-qian-dui-huan-ii-by-leetcode/
举一个例子:amount = 11,可用面值有 2 美分,5 美分和 10 美分。 请注意,数量是无限的
其中第二行表示硬币为0的情况
第三行表示只有面值为2美分的情况
第四行表示只有面值为2美分和5美分的情况
第五行表示有面值为2美分和5美分和10美分的情况
当前行是在上一行基础上转移而来,转移:dp[x]+=dp[x-coin]
C++代码示例:
dp[i]表示第1~k枚硬币凑成i金额的组合数
状态转移
dp[i]=dp[i]+dp[i-coin];当i>=coin时
dp[i]=dp[i];当i<coin
base case:
dp[i]={1,0,0,...,0}
int change(int amount, vector<int>& coins) {
if(amount==0) return 1;
int N=coins.size();
if(N==0) return 0;
vector<int> dp(amount+1);
dp[0]=1;
//注意下面内外循环不能交换顺序
for(auto& coin:coins){
for(int i=0;i<=amount;i++){
if(i>=coin)
dp[i]+=dp[i-coin];
}
}
return dp[amount];
}