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] 一种最优组合
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

时间复杂度

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

复制代码
O(amount × n)

空间复杂度

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

复制代码
O(amount)
相关推荐
程序员小八7772 小时前
上海百度B端java后端日常实习一面
java·开发语言
mldong3 小时前
零依赖的秘密:8 大 SPI 设计
java·架构
203号居民4 小时前
LeetCode hot 100 —41. 缺失的第一个正数
数据结构·算法·leetcode
Diligently_5 小时前
Anolis 系统更新与密码设置&磁盘扩容
java·后端·spring
mqiqe5 小时前
线程调度与 Schedulers:Project Reactor 并发模型的核心引擎
java·开发语言·网络
我命由我123455 小时前
Android 开发问题:unexpected element <service> found in <manifest>
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
正在走向自律6 小时前
飞算JavaAI能搞定充电站故障处置吗?
java·ai编程·java开发·飞算javaai·java代码生成·ai coding模型·springboo
用户40966601317516 小时前
Guava 不止有 Lists 和 Maps:Cache、EventBus、Multimap 实战
java·后端
一只叫煤球的猫6 小时前
从 Java 21 到 Java 25:ThreadLocal 以外的选择—— ScopedValue
java·后端·架构