518. 零钱兑换 II
一、题目
给你一个整数数组 coins
表示不同面额的硬币,另给一个整数 amount
表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0
。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
示例 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
提示:
1 <= coins.length <= 300
1 <= coins[i] <= 5000
coins
中的所有值 互不相同0 <= amount <= 5000
二、题解
方法一:完全背包问题的变体(版本1)
题目理解
这道题目是一个动态规划问题,需要计算凑成总金额的硬币组合数。给定一组硬币的面额数组 coins
和一个总金额 amount
,要求计算有多少种不同的组合方式来凑成总金额。每个硬币的面额都可以被使用无限次。
动态规划思路
我们可以将硬币问题与完全背包问题联系起来:
- 将硬币的面额视为物品的重量。
- 将总金额视为背包的容量。
- 将计算硬币组合数的问题视为在完全背包问题中计算组合数量的变种。
- 定义状态
我们需要定义一个状态来表示问题的子问题和最优解。在这个问题中,我们可以使用二维数组 dp[i][j]
来表示前 i
种硬币组成总金额 j
的组合数。其中,i
表示考虑的硬币种类数量,j
表示总金额。
- 初始化状态
我们需要初始化状态数组 dp
,确保其初始值是正确的。在这里,可以看到 dp[i][0]
应该初始化为0,因为没有硬币可供选择。当 i % coins[0] == 0
时dp[0][i]
应该初始化为1,因为i可以由整数个第一个硬币组成。
- 状态转移方程
接下来,我们需要找到状态之间的转移关系,即如何从子问题的最优解推导出原问题的最优解。在这个问题中,状态转移方程如下:
- 如果当前总金额
j
小于硬币面额coins[i]
,则无法将硬币i
加入组合,所以dp[i][j] = dp[i-1][j]
,表示不使用硬币i
。 - 如果
j
大于等于硬币面额coins[i]
,我们可以选择使用硬币i
或者不使用。因此,dp[i][j]
等于两者之和:- 不使用硬币
i
,即dp[i-1][j]
。 - 使用硬币
i
,即dp[i][j - coins[i]]
,这里的dp[i][j - coins[i]]
表示在考虑硬币i
时,总金额减去硬币i
的面额后的组合数。
- 不使用硬币
- 填充状态表格
通过上述状态转移方程,我们可以通过双重循环遍历所有的子问题,从而填充状态表格 dp
。外层循环遍历硬币种类 i
,内层循环遍历总金额 j
,根据状态转移方程更新 dp[i][j]
。
- 获取最终答案
最后,我们可以通过 dp[coins.size() - 1][amount]
来获取问题的最终答案,即考虑了所有硬币种类并且总金额为 amount
时的组合数。
代码解析
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<vector<int>> dp(coins.size(), vector<int>(amount + 1, 0));
for (int i = 0; i <= amount; i++) {
if (i % coins[0] == 0) {
dp[0][i] = 1;
}
}
for (int i = 1; i < coins.size(); i++) {
for (int j = 0; j <= amount; j++) {
if (j < coins[i]) {
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i]];
}
}
}
return dp[coins.size() - 1][amount];
}
};
-
创建一个二维数组
dp
,其中dp[i][j]
表示前i
种硬币组成总金额j
的组合数。 -
初始化
dp[0][j]
,即考虑只有一种硬币时,对应总金额j
的组合数。如果j
可以被第一种硬币整除,那么dp[0][j]
初始化为1,表示有一种组合方式,即只使用第一种硬币。 -
通过嵌套的循环遍历硬币种类
i
和总金额j
,根据状态转移方程更新dp[i][j]
。如果j
小于硬币面额coins[i]
,则dp[i][j]
等于dp[i-1][j]
,否则dp[i][j]
等于dp[i-1][j] + dp[i][j - coins[i]]
。 -
最后返回
dp[coins.size() - 1][amount]
,即考虑了所有硬币种类并且总金额为amount
时的组合数。
方法二:完全背包问题变体(版本2)
- 定义状态
使用一维数组 dp
,其中 dp[i]
表示总金额 i
的组合方式数量。
- 初始化状态
接下来,我们需要初始化状态数组 dp
,确保其初始值是正确的。在这里,可以看到 dp[0]
应该初始化为1,因为总金额为0时,只有一种组合方式,那就是什么硬币都不选。
- 状态转移方程
然后,我们需要找到状态之间的转移关系,即如何从子问题的最优解推导出原问题的最优解。状态转移方程如下:
- 对于每个硬币面额
coins[i]
,我们可以选择使用该硬币或不使用。 - 如果我们选择使用硬币
coins[i]
,那么dp[j]
应该等于dp[j] + dp[j - coins[i]]
,表示在考虑硬币coins[i]
时,总金额j
的组合方式数量应该加上总金额j - coins[i]
的组合方式数量。 - 如果我们选择不使用硬币
coins[i]
,那么dp[j]
保持不变。
- 填充状态数组
通过上述状态转移方程,我们可以通过循环遍历所有的子问题,从而填充状态数组 dp
。外层循环遍历硬币的面额 i
,内层循环遍历总金额 j
,根据状态转移方程更新 dp[j]
。
- 获取最终答案
最后,我们可以通过 dp[amount]
来获取问题的最终答案,即总金额为 amount
时的组合方式数量。
代码解析
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];
}
};
-
创建一个一维数组
dp
,其中dp[i]
表示总金额i
的组合方式数量。 -
初始化
dp[0]
为1,因为总金额为0时,只有一种组合方式,即不选硬币。 -
通过嵌套的循环遍历硬币面额
coins[i]
和总金额amount
,根据状态转移方程dp[j] += dp[j - coins[i]]
来更新dp[j]
。这表示在考虑硬币coins[i]
时,总金额j
的组合方式数量应该加上总金额j - coins[i]
的组合方式数量。 -
最后返回
dp[amount]
,即总金额为amount
时的组合方式数量。
先遍历物品后遍历背包vs先遍历背包后遍历物品
先遍历物品后遍历背包(组合问题):
如果我们选择先遍历物品后遍历背包,那么我们的状态 dp[j]
表示的是总金额为 j
时的硬币组合数量。在这种情况下,我们考虑了每个硬币,并决定是否将其放入组合。这导致了我们计算的是硬币的组合数量,而不考虑硬币的排列顺序。
先遍历背包后遍历物品(排列问题):
如果我们选择先遍历背包后遍历物品,那么我们的状态 dp[j]
表示的是总金额为 j
时的硬币排列数量。在这种情况下,我们考虑了每个背包容量,然后决定放入哪些硬币。这导致了我们计算的是硬币的排列数量,考虑了硬币的顺序。
话比较抽象,但是我把测试代码写在下面了,大家可以复制过去体会体会。
测试代码1(先遍历物品后遍历背包):
#include <iostream>
using namespace std;
#include <vector>
void print(vector<int>& vec) {
for (auto i : vec) {
cout << i<<" ";
}
cout << endl;
}
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]];
}
print(dp); //i每遍历一次,打印一次dp数组
}
return dp[amount];
}
};
int main()
{
Solution s;
vector<int> coins;
coins.push_back(1); coins.push_back(2); coins.push_back(5);
int result;
result = s.change(5,coins);
cout << "result:" << result << endl;
}
测试代码2(先遍历背包后遍历物品):
#include <iostream>
using namespace std;
#include <vector>
void print(vector<int>& vec) {
for (auto i : vec) {
cout << i << " ";
}
cout << endl;
}
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int> dp(amount + 1, 0);
dp[0] = 1;
for (int j = 0 ; j <= amount; j++) {
for (int i = 0; i < coins.size(); i++) {
if(j >= coins[i]) dp[j] += dp[j - coins[i]];
}
print(dp);
}
return dp[amount];
}
};
int main()
{
Solution s;
vector<int> coins;
coins.push_back(1); coins.push_back(2); coins.push_back(5);
int result;
result = s.change(5, coins);
cout << "result:" << result << endl;
}