给你一个整数数组 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 <= 121 <= coins[i] <= 231 - 10 <= amount <= 104
1. 生活案例:超市收银员的职业病
想象你是一个超市收银员。现在有一个顾客要买 块钱的东西,他给了你一张大票,你需要找给他 元零钱。
-
你的任务:用手里现有的硬币(比如 元、 元、 元)凑够 元,但老板要求你尽量少给硬币(为了省硬币)。
-
你的策略:
-
当你需要凑 元时,你会想:
- 如果我最后给出一枚 元,那我只需要知道“凑够 元最少要几枚硬币”;
- 如果我最后给出一枚 元,那我只需要知道“凑够 元最少要几枚硬币”;
- 如果我最后给出一枚 元,那我只需要知道“凑够 元最少要几枚硬币”。
-
你对比这三种方案,哪种用的总个数最少,你就选哪种。
-
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. 为什么代码这样写?(核心思维)
-
自底向上 (Bottom-Up) :我们不是直接去算 元,而是先把 元、 元、 元... 最少怎么凑都算出来存在
dp数组里。 -
最优子结构:
-
凑齐 元的最优解,一定是从凑齐 元、 元或 元的最优解转移过来的。
-
公式:
-
-
防止走弯路:
dp数组就像你的记事本。当你算 元需要用到 元的结果时,直接翻本子(看dp[6]),不需要重新计算。
总结
这道题的核心就在于**“查表”**。每个金额都遍历所有硬币面值,看看“从哪个面值跳过来步数最短”。