注意几件事情:
-
dp
数组的每一个元素的值有上限,因为硬币的数值都是正整数,因此需要的硬币数量最多不会超过amount + 1
; - 从可以转移的地方开始计算即可;
- 和完全背包问题产生联系。
方法一:动态规划
使用最基本的「动态规划」的思路。
第 1 步:状态定义
dp[i]
表示凑成金额为 i
的最少硬币数。
第 2 步:状态转移方程
根据题目示例 1:coins = [1, 2, 5]
,amount = 11
。很容易想到:
总金额 11
,根据可以选择的硬币列表 [1, 2, 5]
可以这样拆解:
-
11 = 1 + (11 - 1)
:表示11
可以拆成面值为1
的 1 个硬币和凑成面值为10
的最小硬币数之和; -
11 = 2 + (11 - 2)
:表示11
可以拆成面值为2
的 1 个硬币和凑成面值为9
的最小硬币数之和; -
11 = 5 + (11 - 5)
:表示11
可以拆成面值为5
的 1 个硬币和凑成面值为6
的最小硬币数之和;
三者取最小值。下面写出状态转移方程:
d p [ i ] = min { 1 + d p [ i − c o i n s [ j ] ] } dp[i] = \min \{1 + dp[i - coins[j]]\} dp[i]=min{1+dp[i−coins[j]]}
这里 j
是硬币列表的序号。注意 i - coins[j]
必须大于等于 0
,才有意义。
此外,需注意:有两个非常特殊的状态:
-
dp[0]
,0
这个面值是一个在填表的时候被参考的数值,如果总面值为 5,刚好有个硬币的面值也为 5 ,此时dp[5] = 1 + dp[5 - 5] = 1
,故设置dp[0] = 1
; - 也有可能出现例 2 的情况,此时需要设置
dp[i] = - 1
,这道题问的又是最小值,因此,初始化的时候不能设置为0
,应该设置为一个足够大的数。
第 3 步:初始化
(上面已经讨论过了)
第 4 步:输出
输出 dp[amount]
即可。
第 5 步:是否可以空间优化
每一步都有可能用到之前的状态,因此不能压缩。
再次提醒自己:初始化的时候,因为要比较的是最小值,因此不能初始化为 − 1 -1 −1。并且这里 − 1 -1 −1 有专门的含义。
Java 代码:
import java.util.Arrays;
public class Solution {
public int coinChange(int[] coins, int amount) {
// 给 0 占位
int[] dp = new int[amount + 1];
// 注意:因为要比较的是最小值,这个不可能的值就得赋值成为一个最大值
Arrays.fill(dp, amount + 1);
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int coin : coins) {
if (i - coin >= 0 && dp[i - coin] != -1) {
dp[i] = Math.min(dp[i], 1 + dp[i - coin]);
}
}
if (dp[i] == amount + 1) {
dp[i] = -1;
}
}
if (dp[amount] == amount + 1) {
dp[amount] = -1;
}
return dp[amount];
}
}
可以用“完全背包”的模板。(代码是正确的,原因我还没有想得特别清楚。)
import java.util.Arrays;
public class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1);
dp[0] = 0;
for (int coin : coins) {
for (int i = coin; i <= amount; i++) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
if (dp[amount] == 9999) {
dp[amount] = -1;
}
return dp[amount];
}
}
注意:1、初始化的时候,赋值成一个很大的数就可以了,int 的最大值就有可能造成越界;
2、不要在代码中做打印输出,会拖慢时间。