# [518] 零钱兑换 II
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 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 位符号整数
这是一道完全背包问题。
所谓完全背包问题,就是指每个物品数量无限,可以取多次的背包问题。
先说答案,状态转移方程: dp[i][j] = dp[i - 1][j] + (j > coins[i-1][j] ? dp[i][j-coin[i-1]]) : 0)
遍历一遍所有硬币种类,遍历次数 +1 相当于将自己可以选择的硬币种类增加一种。对于每次循环来说,共有取当前种类硬币和不取当前种类硬币两种情况。
如果不取当前硬币,那么该硬币是否可以选择对结果没有影响,因此结果与之前得到的情况数总和相同。即 dp[i][j] = dp[i-1][j]
如果取当前硬币,那么结果即为在之前得到的情况数总和的基础上,加上取该硬币之前的剩余价值(即 目标价值-当前硬币价值)的组成情况。dp[i][j] = dp[i][j] + dp[i][j - coins[i - 1]]
计算时注意边界情况:
- 若没有钱币种类可选(即 coins.length = 0),则没有方案可以选择。
- 若 amount=0,有且只有不取任何钱币一种情况。
var change = function(amount, coins) {
// 初始化二维数组,并初始化第 0 行
const dp = new Array(coins.length + 1).fill(0).map(x => new Array(amount + 1).fill(0));
dp[0][0] = 1;
for (let i = 1; i <= coins.length; i++) {
dp[i][0] = 1;
for (let j = 1; j <= amount; j++) {
dp[i][j] = dp[i - 1][j];
if (j - coins[i - 1] >= 0) {
dp[i][j] += dp[i][j - coins[i - 1]];
}
}
}
return dp[coins.length][amount];
};
由于状态上述转移方程仅依赖 i-1,因此可以转化为一维数组来解节省空间。
以测试数据为例,dp[i] 表示当前 amount 下的所有硬币组成种类数,那么应当等于最后一个硬币为 5、2、1 的硬币种类组成数之和,即 dp[i] = dp[i-5] + dp[i-2] + dp[i-1]。
由此可得出状态转移方程:对每一种硬币 coin, dp[i] = SUM(dp[i-coin])
需要注意的时,转化成一维数组时,在循环时有可能会记入重复情况,如 2+1 和 1+2 会被算成两次。因此循环的顺序一定要注意一下。
function change(amount: number, coins: number[]): number {
const dp = Array(amount + 1).fill(0);
dp[0] = 1;
coins.forEach(coin => {
for (let i = 1; i < amount + 1; i++) {
if (i - coin >= 0) {
dp[i] += dp[i - coin];
}
}
});
return dp[amount];
}