题目
题目来源:力扣(LeetCode)
给你一个整数数组 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
示例 4:
输入:coins = [1], amount = 1
输出:1
示例 5:
输入:coins = [1], amount = 2
输出:2
思路分析
对于这道题,以示例1 coins = [1, 2, 5], amount = 11 为例:
凑足总额为 11 的最少硬币数,可以考虑组合中的最后一个硬币分别是1,2,5的情况,比如
- 最后一个硬币是1的话,最少硬币数应该为【组成10的最少硬币数】+ 1枚(1块硬币)
- 最后一个硬币是2的话,最少硬币数应该为【组成9的最少硬币数】+ 1枚(2块硬币)
- 最后一个硬币是5的话,最少硬币数应该为【组成6的最少硬币数】+ 1枚(5块硬币)
那么凑足总额为 11 的最少硬币数为:dp[11] = min (dp[10] + 1, dp[9] + 1, dp[6] + 1)
把上面的分析转换成状态:
1、状态定义
dp[j]:凑足总额为 j 所需的最少硬币数为 dp[j]
2、确定递推公式
要得到 dp[j] (包含了面值 coins[i]),只有一个来源,那就是 dp[j - coins[i]] (没有包含 coins[i] 这个面值);
凑足总额为 j - coins[i] 的最少硬币数为 dp[j -coins[i]],那么只需要再加上一个面值为 coins[i] 的硬币,即 dp[j - coins[i]] + 1 即可得到 dp[j],即 dp[j] = dp[j - coins[i]] + 1
dp[j] 要取所有 dp[j - coins[i]] + 1 中最少的,因此可得递推公式为:**dp[j] = min(dp[j - coins[i]] + 1, dp[j])**
3、状态初始化
首先凑足总金额为 0 所需硬币的个数一定是 0,那么 dp[0] = 0;
那么其他下标对应的数值呢?
考虑到递推公式的特性,dp[j] 必须初始化为一个最大的数,否则就会在min(dp[j - coins[i]] + 1, dp[j])比较的过程中被初始值覆盖。
所以下标非0的元素都应该是最大值。
dp 初始化代码:
let dp = new Array(amount + 1).fill(Number.MAX_VALUE);
dp[0] = 0;
4、确定遍历顺序
本题求硬币最小个数,那么硬币有顺序和没有顺序都可以,都不影响硬币的最小个数。
所以本题的两个for循环的关系是:外层for循环遍历物品,内层for遍历背包或者外层for遍历背包,内层for循环遍历物品都是可以的!
那么我采用coins放在外循环,target在内循环的方式。
遍历顺序为:coins(物品)放在外循环,target(背包)在内循环。且内循环正序。
代码实现:
/**
* @param {number[]} coins
* @param {number} amount
* @return {number}
*/
var coinChange = function (coins, amount) {
// dp[j]必须初始化为一个最大的数,否则就会在min(dp[j - coins[i]] + 1, dp[j])比较的过程中被初始值覆盖
let dp = new Array(amount + 1).fill(Number.MAX_VALUE);
// 凑足总金额为0所需钱币的个数一定是0,初始化 dp[0] = 0
dp[0] = 0;
for (let i = 0; i < coins.length; i++) { // 遍历物品
for (let j = coins[i]; j <= amount; j++) { // 遍历背包
if (dp[j - coins[i]] != Number.MAX_VALUE) { // 如果dp[j - coins[i]]是初始值则跳过
dp[j] = Math.min(dp[j - coins[i]] + 1, dp[j]);
}
}
}
if (dp[amount] == Number.MAX_VALUE) return -1;
return dp[amount];
};