【中等】322. 零钱兑换

0 阅读1分钟

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

你可以认为每种硬币的数量是无限的。

示例 1:

输入: coins = [1, 2, 5], amount = 11
输出: 3 
解释: 11 = 5 + 5 + 1

示例 2:

输入: coins = [2], amount = 3
输出: -1

示例 3:

输入: coins = [1], amount = 0
输出: 0

提示:

  • 1 <= coins.length <= 12
  • 1 <= coins[i] <= 231 - 1
  • 0 <= amount <= 104

1. 生活案例:超市收银员的职业病

想象你是一个超市收银员。现在有一个顾客要买 1111 块钱的东西,他给了你一张大票,你需要找给他 1111 元零钱。

  • 你的任务:用手里现有的硬币(比如 11 元、22 元、55 元)凑够 1111 元,但老板要求你尽量少给硬币(为了省硬币)。

  • 你的策略

    • 当你需要凑 1111 元时,你会想:

      • 如果我最后给出一枚 55 元,那我只需要知道“凑够 66 元最少要几枚硬币”;
      • 如果我最后给出一枚 22 元,那我只需要知道“凑够 99 元最少要几枚硬币”;
      • 如果我最后给出一枚 11 元,那我只需要知道“凑够 1010 元最少要几枚硬币”。
    • 你对比这三种方案,哪种用的总个数最少,你就选哪种。


2. 代码解析与“生活化”注释

这段代码展示了如何通过从小金额开始计算,一步步推导出大金额的最优解。

JavaScript

/**
 * @param {number[]} coins - 你兜里有的硬币面值,比如 [1, 2, 5]
 * @param {number} amount - 目标找零金额,比如 11
 * @return {number} - 最少硬币个数
 */
var coinChange = function (coins, amount) {
    // dp[i] 代表:凑齐金额 i 所需的最少硬币个数
    // 初始值填 amount + 1(一个不可能达到的大数),就像是给每个金额先标个“暂无方案”
    let dp = new Array(amount + 1).fill(amount + 1);

    // 基础情况:凑够 0 元只需要 0 枚硬币
    dp[0] = 0;

    // i 代表当前我们要计算的“目标找零金额”,从 1 元一直算到目标 amount 元
    for (let i = 1; i <= amount; i++) {
        // 对于每一个金额 i,尝试每一枚可用的硬币
        for (let coin of coins) {
            // 如果当前金额 i 比硬币面值大,说明这枚硬币“用得掉”
            if (i >= coin) {
                // 生活化解释:
                // 我尝试用掉这枚面值为 coin 的硬币
                // 那么总个数 = 凑齐 (i - coin) 元的最少个数 + 1 (当前这枚)
                // 我们拿这个结果跟之前记录的最佳方案 dp[i] 比,取更小的那个
                dp[i] = Math.min(dp[i], dp[i - coin] + 1);
            }
        }
    }

    // 如果最后 dp[amount] 还是那个大数,说明这钱根本凑不出来(比如只有2元硬币却要凑3元)
    // 凑得出来就返回结果,凑不出来返回 -1
    return (dp[amount] > amount) ? -1 : dp[amount];
};

3. 为什么代码这样写?(核心思维)

  1. 自底向上 (Bottom-Up) :我们不是直接去算 1111 元,而是先把 11 元、22 元、33 元... 最少怎么凑都算出来存在 dp 数组里。

  2. 最优子结构

    • 凑齐 1111 元的最优解,一定是从凑齐 1010 元、99 元或 66 元的最优解转移过来的。

    • 公式:

      dp[i]=min(dp[icoin]+1)dp[i] = \min(dp[i - coin] + 1)

  3. 防止走弯路dp 数组就像你的记事本。当你算 1111 元需要用到 66 元的结果时,直接翻本子(看 dp[6]),不需要重新计算。

总结

这道题的核心就在于**“查表”**。每个金额都遍历所有硬币面值,看看“从哪个面值跳过来步数最短”。