322. 零钱兑换 (coin change)

4,053 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第8天,点击查看活动详情

322. 零钱兑换:给你一个整数数组 coinscoins,表示不同面额的硬币;以及一个整数 amountamount,表示总金额。计算并返回可以凑成总金额所需的最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 1-1 。你可以认为每种硬币的数量是无限的。

示例
输入: coins=[1,2,5]coins = [1, 2, 5], amount=11amount = 11
输出: 33
解释: 11=5+5+111 = 5 + 5 + 1

中规中矩的动态规划

寻找一下最优子结构:我们手上有 coin1,coin2,...,coinncoin_1,coin2,...,coin_n(其中 n=coins.lengthn=coins.length)等面值的货币,想计算出 amountamount 金额可兑换的最少硬币数量。设想,如果可以知道 amountcoin1amount-coin_1 需要的最少硬币数量,再其基础上加 11(即再选择一枚 coin1coin_1 面值的硬币),就可以知道 amountamount 需要最少的硬币数量。这是独立子问题之一,因为我们还需要知道 amountcoin2,...,amountcointnamount-coin_2,...,amount - coint_n 等一系列独立子问题的解,获得所有解的最小值之后,并再其基础上加1,才能获得最终的解,即 amountamount 需要最少的硬币数量。

1、定义 dp 状态数组

dp[i]dp[i] 是金额为 ii 时所需要的最少银币数量,其中 i[0,amount]i \in [0,amount]

2、定义 dp 状态转移方程

dp[i]=min(dp[icoin]coincoins)+1dp[i] = min(dp[i-coin] | coin \in coins) + 1

3、定义 dp 初始状态

金额为 00 时,是不需要硬币的,即 dp[0]=0dp[0] =0

4、确定遍历顺序

第一层循环:从 i=1i=1i=amounti= amount;

第二层循环:从 j=0j=0j=coins.length1j=coins.length-1

5、确定最终返回值

dp[amount]dp[amount]1-1

💥NOTE:既然是求最小值,初始化 dpdp 数组时一定要用一个较大的值去填充,当循环结束时,如果 dp[amount]dp[amount] 等于这个较大的值,说明兑换硬币失败,返回 1-1 ,否则返回 dp[amount]dp[amount]

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:为什么初始化 dpdp 数组要填充 amount+1amount +1

  • 首先,题意要求“最小值”,所以初始化 dpdp 时肯定要填充一个较大的值;

  • 其次,题意里规定硬币的最小面值为 11 ,因此所需要的银币个数最多为 amountamount 个;

  • 最后,amount+1amount+1 是一个“最大值”的下限(当然可以设置成 infinityinfinity)。把它作为“标记数”,当循环结束时,如 dp[amount]===amount+1dp[amount] === amount + 1 ,就说明 amoutamout 金额不能被兑换成零钱,直接返回 1-1 即可。

Q2:为什么金额 ii 小于 coin[j]coin[j] 时要忽略?

  • 首先,icoin[j]i- coin[j] 作为索引,的目的是从 dpdp 数组中寻找上一个状态需要的最小硬币个数;

  • 其次,icoin[j]<0i- coin[j] < 0 在数组 dpdp 会越界,虽然在JS引擎内访问越界索引不会报错,但会获得 undefined,该值在 Math.min 计算下会返回 NaN,影响后续循环与计算;

  • 最后,对于数组越界的索引对应的值,可以赋值一个较大的数值(如 amount+1amount+1),这样不会影响后续循环与计算。

参考

# 重识动态规划

# 重识背包问题(上)

# 重识背包问题(下)