给你一个整数数组
coins
,表示不同面额的硬币;以及一个整数amount
,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回-1
。
你可以认为每种硬币的数量是无限的。
来源:力扣(LeetCode)
int[] coins
;总金额:int amount
dp[i]
:凑成总金额 i
需要的最少硬币数dp[i] = min (dp[i-c]) + 1 // `c` in the array `coins`
public int coinChange(int[] coins, int amount) {
if (amount == 0) return 0;
int[] dp = new int[amount + 1]; // `dp[amount]` is valid
Arrays.fill(dp, -1); // 初识状态:(对应题干)没有任何一种硬币组合能组成总金额 i,则为`-1`
dp[0] = 0;
/**
* 这步是为了后面 dp[i] = min + 1; 也能够处理 i == c 的情况。
* 其实,删除这步也可以,在 for循环 中分开考虑:
if (i - c == 0) dp[i] = 1;
if (i - c > 0) ...
*/
for (int i = 1; i <= amount; i++) { // i 直接从 1 开始 是因为 amount == 0 的情况在最开始已经考虑过了
int min = Integer.MAX_VALUE;
for (int c : coins) {
if (i - c >= 0 && dp[i - c] != -1) min = Math.min(min, dp[i - c]);
// else dp[i] 保持 -1,因为i < c <=> 总金额 i < 已有的硬币额 c)
}
if (min != Integer.MAX_VALUE) dp[i] = min + 1;
}
return dp[amount];
}
非常好地体现了 dp[i] = min (dp[i-c]) + 1
:先在 dp[i-c]
中找到最小值,然后让 dp[i] = min + 1
。
其实:
dp[i] = min (dp[i - c]) + 1 = min (dp[i-c] + 1)
所以我们可以省去变量 min
:
for (int i = 1; i <= amount; i++) {
int dp[i] = Integer.MAX_VALUE;
for (int c : coins) {
if (i - c >= 0 && dp[i - c] != -1) dp[i] = Math.min(dp[i], dp[i - c] + 1); // `i-c > -1` => `dp[i-c]` is valid
}
if (dp[i] != Integer.MAX_VALUE) dp[i] = -1;
}
发现这样写代码过于繁琐:
min
或者 dp[i]
赋上最大值;dp[i-c] = -1
的项是不能参与的:dp[i-c] = -1
表示 没有任何一种硬币组合能组成总金额 i-c
,自然也就不能通过 (i-c) + c
的方式凑出 i
了;min
或 dp[i]
的情况。如果 数组 dp
的每个元素的初值设为最大值 Integer.MAX_VALUE
可以很好的简化代码:
public int coinChange(int[] coins, int amount) {
if (amount == 0) return 0;
int[] dp = new int[amount+1];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
for (int i = 1; i<=amount; i++) {
for (int c : coins) {
if (i-c >= 0) dp[i] = Math.min(dp[i], dp[i-c] + 1);
//else dp[i] 保持最大值不变
}
}
return dp[amount] == amount+1 ? -1 : dp[amount];
}
如果不想记忆 dp[0] = 0;
,可以像 1.0代码 中注释中说的那样:在循环中将 if (i - c == 0) dp[i] = 1;
单独考虑:
...
// dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int c : coins) {
if (i - c == 0) dp[i] = 1; // 补上。这样也更好理解。
if (i - c > 0) dp[i] = Math.min(dp[i], dp[i-c] + 1);
}
}
...
当然,最大值不一定非要暴力地设成 Integer.MAX_VALUE
,最大值也可以是:amount + 1
:
因为:
coins[i] >= 1
,所以:最多需要amount
个硬币。
由于 amount + 1
在代码中出现频率较高(如:int[] dp = new int[amount+1];
,i <= amount
其实可以写成 i < amount + 1
),所以在 LeetCode官方题解 中,用变量 max
将 amount + 1
存起来了,所以代码还可以写成:
public int coinChange(int[] coins, int amount) {
int max = amount + 1;
int[] dp = new int[max];
Arrays.fill(dp, max);
dp[0] = 0;
for (int i = 1; i < max; i++) {
for (int c : coins) {
if (i-c >= 0) dp[i] = Math.min(dp[i], dp[i-c] + 1);
}
}
return dp[amount] == max ? -1 : dp[amount];
}
最后,因为本题中 for
循环的顺序对结果没有影响,所以可以将 内外层循环交换位置并设置 i
的起始值 以减少对一些情况的判断(原循环中,当 i
小于 最小的硬币额度 时,内层循环没有必要再一一遍历硬币额度了):
public int coinChange(int[] coins, int amount) {
int max = amount + 1;
int[] dp = new int[max];
Arrays.fill(dp, max);
dp[0] = 0;
for (int c : coins) {
for (int i = c; i < max; i++) {
if (i-c >= 0) dp[i] = Math.min(dp[i], dp[i-c] + 1);
}
}
return dp[amount] == max ? -1 : dp[amount];
}
dp
元素的初值赋上最大值,在最后判断 dp[amount] == max
;dp[0] = 0
。记不住的话在 for
循环里单独考虑 if (i == c) dp[i] = 1;