「这是我参与2022首次更文挑战的第 26 天,活动详情查看:2022首次更文挑战」
题目链接
322. 零钱兑换 - 力扣(LeetCode) (leetcode-cn.com)
题目描述
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
测试用例
示例 1:
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
条件限制
1 <= coins.length <= 121 <= coins[i] <= 231 - 10 <= amount <= 104
题目分析
题目会提供一个硬币种类的数组,以例1 为例,提供面值为 1,2,5 的硬币,且不限数量,然后需要我们使用这 3 种硬币来组合,① 是否可以组合得到 11 这个数? ② 如果可以得到,使用硬币的最小数量是多少?我们可以使用 11 个 1面值的组合得到目标数,也可以使用 2 个 5 + 1 个 1 得到目标数
题目最麻烦的地方,在于这个硬币的种类是不确定的
继续以例1 分析,使用一些数学标记来从 11 往下推导到 1 的硬币组合
f11 = 1 + f10 或 2 + f9 或 5 + f6
f10 = 1 + f9 或 2 + f8 或 5 + f5
f9 = 1 + f8 或 2 + f7 或 5 + f4
....
f5 = 5
...
f2 = 2
f1 = 1
f5 可以使用 1 和 2 组合来实现,但我们需要最小的组合硬币数,因此直接使用一个面值为 5 的硬币
在上面的数学表达式里,具体的面值,全部用 1 替代,然后计算每一个分支,比较选取最小的值返回即可,如 f11 的实际最佳组合为 f11 = 5 + f6 = 5 + 5 + f1 = 5 + 5 + 1,共需 3 枚硬币
这种从目标值回推到基础值的操作,需要我们注意避开重复的值计算
下面分享一种更接地气的方法,直接从 1 推导到 11,也就是从基础值推导到目标值,这种也叫 dp 打表法,推导过程如下
f1 = 1 => 1
f2 = 1 + f1 或 2 => 2
f3 = 1 + f2 或 2 + f1 => 1 + 2
f4 = 1 + f3 或 2 + f2 => 2 + 2
f5 = 1 + f4 或 2 + f4 或 5 => 5
...
这种推导的优势在于,我们只需要从 f1 开始计算,一步步计算到 f11, 中间的每一步的值,都是已经算出来的最优解
由于题目给出的硬币的种类是不确定的,比如,我们使用 [2, 5] 这种组合来推导 f11
f1 = 无 => -1
f2 = 2 => 2
f3 = 2 + f1 => -1
f4 = 2 + f2 => 2 + 2
f5 = 5 或 2 + f3 => 5
f6 = 5 + f1 或 2 + f4 => 2 + 2 + 2
...
如果目标值的所有的组合,如果某项组合使用到的上一步的值为 -1,则这项组合需要抛弃,最后若不存在有效的组合,此项数的值就为 -1,如 f3 的组合推导
剩下的就是根据上面的推导过程,用代码实现
代码实现
/**
* @param {number[]} coins
* @param {number} amount
* @return {number}
*/
var coinChange = function(coins, amount) {
if (amount == 0) return 0;
coins = coins.sort((a, b) => a - b);
let arrs = new Array(amount + 1).fill(-1);
for (let i = 1; i <= amount; i++) {
for (let j = 0; j < coins.length; j++) {
if (i < coins[j]) break;
if (coins[j] == i) {
arrs[i] = 1;
break;
}
let index = i - coins[j];
if (index < 1) continue;
if (arrs[index] == -1) {
continue;
}
if (arrs[i] == -1) {
arrs[i] = arrs[index] + 1;
} else {
arrs[i] = Math.min(arrs[i], arrs[index] + 1);
}
}
}
return arrs[amount];
};