一、题目描述
给定一个整数数组 coins,其中每个元素表示一种硬币的面额;再给定一个整数 amount,表示需要凑出的总金额。
每种硬币都可以使用任意次,请计算凑成总金额所需的最少硬币数量。如果无法凑出该金额,则返回 -1。
例如:
输入:coins = [1, 2, 5],amount = 11
输出:3
解释:11 = 5 + 5 + 1
输入:coins = [2],amount = 3
输出:-1
输入:coins = [1],amount = 0
输出:0
这道题要求的不是硬币组合数量,而是凑出目标金额所需的最少硬币数,可以使用动态规划解决。
二、为什么可以使用动态规划?
假设要凑出金额 11,并且最后使用的硬币面额是 5,那么问题就变成:
先用最少的硬币凑出
11 - 5 = 6,再加上一枚面额为5的硬币。
如果最后使用的是面额为 2 的硬币,则需要先凑出 9;如果最后使用的是面额为 1 的硬币,则需要先凑出 10。
因此,凑出金额 i 的最优解,可以由比 i 更小的金额推导出来。问题具有重复子问题和最优子结构,适合使用动态规划。
三、定义动态规划状态
定义:
dp[i]:凑出金额 i 所需的最少硬币数量
这里的数组下标就是金额。例如:
-
dp[0]表示凑出金额0所需的最少硬币数; -
dp[4]表示凑出金额4所需的最少硬币数; -
dp[11]表示凑出金额11所需的最少硬币数。
最终需要返回的结果就是 dp[amount]。
四、确定初始状态
凑出金额 0 不需要使用任何硬币,因此:
dp[0] = 0;
其余金额暂时还不知道能否凑出,需要先将它们初始化成一个不可能成为正确答案的较大值:
Arrays.fill(dp, amount + 1);
dp[0] = 0;
为什么初始化为 amount + 1?
假设硬币面额都是正整数,凑出金额 amount 最极端的情况,是全部使用面额为 1 的硬币,此时最多需要 amount 枚。
因此,合法答案不可能大于 amount。将初始值设置为 amount + 1,就可以把它当作“无穷大”,表示当前金额暂时无法凑出。
例如 amount = 11 时:
dp = [0, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12]
这里的 12 只是哨兵值,并不表示真的需要 12 枚硬币。
如果使用 Integer.MAX_VALUE,状态转移时执行:
dp[i - coin] + 1
可能发生整数溢出。因此,使用 amount + 1 更安全,也更方便判断最终是否存在答案。
五、推导状态转移公式
计算 dp[i] 时,需要枚举所有硬币 coin,并假设当前选取的最后一枚硬币就是 coin。
如果 coin <= i,说明这枚硬币可以用于凑当前金额。选取它之后,还需要凑出的金额为:
i - coin
凑出这部分金额的最少硬币数已经记录在 dp[i - coin] 中,再加上当前选择的一枚硬币,硬币总数就是:
dp[i - coin] + 1
由于最后一枚硬币存在多种选择,需要在所有可行方案中取最小值:
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
完整状态转移可以表示为:
dp[i] = min(dp[i - coin] + 1)
其中 coin 遍历所有满足 coin <= i 的硬币
核心思想可以概括为:
枚举凑出金额
i时使用的最后一枚硬币,在所有可行选择中,取硬币总数的最小值。
六、以 dp[4] 为例理解转移过程
假设:
coins = [1, 2, 5]
在计算 dp[4] 之前,已经得到:
dp[0] = 0
dp[1] = 1
dp[2] = 1
dp[3] = 2
现在枚举每一种硬币。
1. 最后一枚硬币选择 1
选择一枚面额为 1 的硬币后,还需要凑出:
4 - 1 = 3
凑出金额 3 最少需要 dp[3] = 2 枚硬币,因此当前方案需要:
dp[3] + 1 = 3
对应组合可以是:
2 + 1 + 1
2. 最后一枚硬币选择 2
选择一枚面额为 2 的硬币后,还需要凑出:
4 - 2 = 2
凑出金额 2 最少需要 dp[2] = 1 枚硬币,因此当前方案需要:
dp[2] + 1 = 2
对应组合为:
2 + 2
3. 能否选择硬币 5?
由于:
5 > 4
面额为 5 的硬币不能用于凑金额 4,否则 i - coin 会变成负数,造成数组下标越界。
最终取所有可行方案中的最小值:
dp[4] = min(3, 2) = 2
七、完整执行过程
对于:
coins = [1, 2, 5]
amount = 11
从金额 1 开始依次计算,可以得到:
金额 i | 最少硬币数 dp[i] | 一种最优组合 |
|---|---|---|
| 0 | 0 | 不使用硬币 |
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 2 | 2 + 1 |
| 4 | 2 | 2 + 2 |
| 5 | 1 | 5 |
| 6 | 2 | 5 + 1 |
| 7 | 2 | 5 + 2 |
| 8 | 3 | 5 + 2 + 1 |
| 9 | 3 | 5 + 2 + 2 |
| 10 | 2 | 5 + 5 |
| 11 | 3 | 5 + 5 + 1 |
最终:
dp[11] = 3
所以凑出金额 11 最少需要 3 枚硬币。
八、Java 完整代码
import java.util.Arrays;
class Solution {
public int coinChange(int[] coins, int amount) {
// dp[i] 表示凑出金额 i 所需的最少硬币数量
int[] dp = new int[amount + 1];
// amount + 1 表示当前金额暂时无法凑出
Arrays.fill(dp, amount + 1);
// 凑出金额 0 不需要硬币
dp[0] = 0;
// 依次计算从 1 到 amount 的每个状态
for (int i = 1; i <= amount; i++) {
// 枚举凑出金额 i 时可能使用的最后一枚硬币
for (int coin : coins) {
if (coin <= i) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
// 大于 amount 说明仍为初始哨兵值,目标金额无法凑出
return dp[amount] > amount ? -1 : dp[amount];
}
}
代码中的两层循环分别表示:
-
外层循环枚举需要凑出的金额
i; -
内层循环枚举当前可以使用的最后一枚硬币
coin。
当 coin <= i 时,才能使用该硬币更新 dp[i]。
九、为什么最终无法兑换时返回 -1?
以以下输入为例:
coins = [2]
amount = 3
初始化后:
dp = [0, 4, 4, 4]
计算完成后:
-
dp[2]可以通过一枚硬币2更新为1; -
dp[1]和dp[3]都无法被更新; -
dp[3]仍然等于哨兵值4。
由于合法答案最多为 amount,如果最终满足:
dp[amount] > amount
就说明目标金额无法凑出,应该返回 -1。
十、复杂度分析
设硬币种类数量为 n,目标金额为 amount。
时间复杂度
外层循环遍历 1 到 amount,内层循环遍历全部 n 种硬币,因此时间复杂度为:
O(amount × n)
空间复杂度
使用了一个长度为 amount + 1 的一维数组,因此空间复杂度为:
O(amount)

3500

被折叠的 条评论
为什么被折叠?



