开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第8天,点击查看活动详情
322. 零钱兑换:给你一个整数数组 ,表示不同面额的硬币;以及一个整数 ,表示总金额。计算并返回可以凑成总金额所需的最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 。你可以认为每种硬币的数量是无限的。
| 示例 |
|---|
| 输入: , 输出: 解释: |
中规中矩的动态规划
寻找一下最优子结构:我们手上有 (其中 )等面值的货币,想计算出 金额可兑换的最少硬币数量。设想,如果可以知道 需要的最少硬币数量,再其基础上加 (即再选择一枚 面值的硬币),就可以知道 需要最少的硬币数量。这是独立子问题之一,因为我们还需要知道 等一系列独立子问题的解,获得所有解的最小值之后,并再其基础上加1,才能获得最终的解,即 需要最少的硬币数量。
1、定义 dp 状态数组
是金额为 时所需要的最少银币数量,其中 。
2、定义 dp 状态转移方程
3、定义 dp 初始状态
金额为 时,是不需要硬币的,即
4、确定遍历顺序
第一层循环:从 到 ;
第二层循环:从 到
5、确定最终返回值
或
💥NOTE:既然是求最小值,初始化 数组时一定要用一个较大的值去填充,当循环结束时,如果 等于这个较大的值,说明兑换硬币失败,返回 ,否则返回 。
6、代码示例
/**
* 空间复杂度 O(amount)
* 时间复杂度 O(amount * coins.length)
*/
function coinChange(coins: number[], amount: number): number {
if (amount === 0) {
return 0;
}
const dp = new Array(amount + 1).fill(amount + 1);
dp[0] = 0;
for(let i = 1; i <= amount; i++) {
for(let j = 0; j < coins.length; j++) {
if (coins[j] <= i) {
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
}
// if语句可简化为:dp[i] = Math.min(dp[i], (dp[i - coins[j]] ?? amount) + 1);
}
}
return dp[amount] === amount + 1 ? -1 : dp[amount];
};
总结
Q1:为什么初始化 数组要填充 ?
-
首先,题意要求“最小值”,所以初始化 时肯定要填充一个较大的值;
-
其次,题意里规定硬币的最小面值为 ,因此所需要的银币个数最多为 个;
-
最后, 是一个“最大值”的下限(当然可以设置成 )。把它作为“标记数”,当循环结束时,如 ,就说明 金额不能被兑换成零钱,直接返回 即可。
Q2:为什么金额 小于 时要忽略?
-
首先, 作为索引,的目的是从 数组中寻找上一个状态需要的最小硬币个数;
-
其次, 在数组 会越界,虽然在JS引擎内访问越界索引不会报错,但会获得
undefined,该值在Math.min计算下会返回NaN,影响后续循环与计算; -
最后,对于数组越界的索引对应的值,可以赋值一个较大的数值(如 ),这样不会影响后续循环与计算。