给定不同面额的硬币
coins
和一个总金额amount
。写出函数来计算可以凑成总金额的硬币组合数
。假设每一种面额的硬币有无限个。
这道题是在0-1
背包问题的变形,是完全背包问题,那么什么是完全背包问题呢?
再来回顾一下0-1背包的题目:给你一个可装载重量为W
的背包和N
个物品(每个物品不一样),每个物品有重量和价值两个属性。其中第i
个物品的重量为wt[i]
,价值为val[i]
,现在让你用这个背包装物品,最多能装的价值是多少?
0-1
背包问题中物品的数量是有限的,准确来说每个物品都是独一无二的,而这里的每一种面额的硬币有无限个,这就是完全背包问题
,思路跟0-1背包的思路是一样的,只是状态转移方程略有改变而已。
我们可以根据0-1
背包的思路来解这道题:
我们将不同面额的硬币比作0-1
背包问题中的物品,将总金额比作背包的容量,这样就可以转化为背包问题的模型来进行求解。
- 第一步:明确「状态」和「选择」
状态有两个,就是「背包的容量」和「可选择的物品」,选择就是「装进背包」或者「不装进背包」。
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 计算(选择1,选择2...)
- 明确
dp
数组的定义
dp[i][j]
的定义如下:只使用前 i
个物品,当背包容量为 j
时,有 dp[i][j]
种方法可以装满背包。
转换为硬币兑换的模型就是:只使用coins
中的前i
个硬币的面值,若想凑出金额j
,有 dp[i][j]
种凑法。
base case 为 dp[0][..] = 0, dp[..][0] = 1
。因为如果不使用任何硬币面值,就无法凑出任何金额;如果凑出的目标金额为 0,那么“无为而治”就是唯一的一种凑法。
我们最终目的就是求 dp[N][amount]
,其中 N
为 coins
数组的大小。
- 根据「选择」,思考状态转移的逻辑
如果不使用 coins[i]
这个面值的硬币,那么凑出面额 j
的方法数 dp[i][j]
应该等于 dp[i-1][j]
,继承之前的结果。
如果使用 coins[i]
这个面值的硬币,那么 dp[i][j]
应该等于 dp[i][j-coins[i-1]]
。
首先由于 i
是从 1 开始的,所以 coins
的索引是 i-1
时表示第 i
个硬币的面值。
dp[i][j-coins[i-1]]
也不难理解,如果你决定使用这个面值的硬币,那么就应该关注如何凑出金额 j - coins[i-1]
。
综上就是两种选择,而我们想求的 dp[i][j]
是「共有多少种凑法」,所以 dp[i][j]
的值应该是以上两种选择的结果之和:
class Solution {
int change(int amount, int[] coins) {
int n = coins.length;
int[][] dp = amount int[n + 1][amount + 1];
for (int i = 0; i <= n; i++) {
dp[i][0] = 1;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= amount; j++){
if (j - coins[i-1] >= 0){
dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i-1]];
}else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[n][amount];
}
}
而且,我们通过观察可以发现,dp
数组的转移只和 dp[i][..]
和 dp[i-1][..]
有关:
所以可以压缩状态,进一步降低算法的空间复杂度:
class Solution {
public int change(int amount, int[] coins) {
int n = coins.length;
int[] dp = new int[amount + 1];
dp[0] = 1;
for (int i = 0; i < n; i++){
for (int j = 1; j <= amount; j++){
if (j - coins[i] >= 0){
dp[j] += dp[j-coins[i]];
}
}
}
return dp[amount];
}
}
甚至可以这样:
class Solution {
public int change(int amount, int[] coins) {
int[] dp = new int[amount + 1];
dp[0] = 1;
for (int coin : coins) {
// j直接从coin开始,避免了j - coins[i] >= 0的判断
for (int j = coin; j <= amount; j++) {
dp[j] += dp[j - coin];
}
}
return dp[amount];
}
}
时间复杂度 O(N*amount)
空间复杂度 O(amount)