LeetCode 322:零钱兑换——Java 动态规划详解

一、题目描述

给定一个整数数组 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]一种最优组合
00不使用硬币
111
212
322 + 1
422 + 2
515
625 + 1
725 + 2
835 + 2 + 1
935 + 2 + 2
1025 + 5
1135 + 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

时间复杂度

外层循环遍历 1amount,内层循环遍历全部 n 种硬币,因此时间复杂度为:

O(amount × n)

空间复杂度

使用了一个长度为 amount + 1 的一维数组,因此空间复杂度为:

O(amount)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值