518. 零钱兑换 II (coin change II)

3,883 阅读2分钟

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

518. 零钱兑换 II 题目描述:给你一个整数数组 coinscoins 表示不同面额的硬币,另给一个整数 amountamount 表示总金额。请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 00。假设每一种面额的硬币有无限个。

示例
输入: amount=5,coins=[1,2,5]amount = 5, coins = [1, 2, 5]
输出: 44
解释: 有四种方式可以凑成总金额:
15=51、5=5
25=2+2+12、5=2+2+1
35=2+1+1+13、5=2+1+1+1
45=1+1+1+1+14、5=1+1+1+1+1

中规中矩的动态规划

典型的 完全背包 组合问题:每种物品(硬币)无限选择,仅考虑整体,不考虑排序,背包容量为 amountamount

1、确定 dp 数组及含义

💥 dp[i][j]dp[i][j] 为选择 [0,i][0,i] 区间时,凑成总和为 jj 的组合数,其中 i[0,n)i \in[0,n)n=coins.lengthn= coins.lengthj[0,amount]j\in[0,amount]

2、确定 dp 状态方程

如果不选择 coins[i]coins[i],那么 dp[i][j]=dp[i1][j]dp[i][j]=dp[i-1][j]

其中,包括两种情况,

  • 主动放弃 coins[i]coins[i],与背包容量无关;

  • 被动放弃 coins[i]coins[i],容量所限,即 j<k×coins[i]j < k \times coins[i]kk 为正整数。

如果选择 coins[i]coins[i],则有,dp[i][j]+=dp[i1][jk×coins[i]]dp[i][j] += dp[i-1][j - k \times coins[i]],其中,jk×coins[i]j \ge k \times coins[i]

3、确定 dp 初始状态

当背包容量恒为 00j=0j=0),从 [0,i][0,i] 区间选择银币凑出总和为 00 的组合数只有 11 种(不选择银币),即 dp[i][0]=1dp[i][0] = 1

  • 要从 [0,0][0,0] 区间选择银币(只能选择 coins[0]coins[0])凑出总和为 jj。如果容量为 jj 的背包恰能装下 kkcoins[0]coins[0],其中 kk 是正整数,那么组合数即为 11,否则为 00。即,dp[0][j]=j mod coins[0]==0 ? 1:0dp[0][j] = j \space mod \space coins[0] ==0\space ? \space 1 :0

4、确定遍历顺序

对于完全背包 组合 问题,应先遍历物品(coinscoins),再遍历背包(amountamount),可参考 # 重识背包问题(下)

  • 外循环遍历从 i=1i=1i=n1i= n-1

  • 内循环遍历从 j=1j=1j=amountj=amount

5、确定返回值

回归到 dpdp 定义中,返回值即为 dp[n1][amount]dp[n- 1][amount]

6、代码示例

/**
 * 空间复杂度 O(amount * n),n是数组coins的长度
 * 时间复杂度 O(amount * n)
 */
function change(amount: number, coins: number[]): number {
    const n = coins.length;
    const dp = Array.from({ length: n }, () => new Array(amount + 1).fill(0));

    for (let i = 0; i < n; i++) {
        dp[i][0] = 1;
    }

    // 背包容量不能被coins[0]整除,说明这种组合不成立,组合数为0,否则为1
    for (let j = 1; j <= amount; j++) {
        dp[0][j] = (j % coins[0]) ? 0 : 1;
    }

    for (let i = 1; i < n; i++) {
        for (let j = 1; j <= amount; j++) {
            dp[i][j] = dp[i - 1][j];
            for (let k = 1; k * coins[i] <= j; k++) {
                dp[i][j] += dp[i - 1][j - k * coins[i]]
            }
        }
    }

    return dp[n - 1][amount];
};

思维提升-哨兵

"方法1: 中规中矩的动态规划"中初始化条件较为复杂,我们在 dp[i][j]dp[i][j]ii 方向的作用域拓展一个“哨兵”,辅助计算。

1、确定 dp 状态数组

dp[i][j]dp[i][j] 表示选择 [0,i)[0,i) 区间时,凑成总和为 jj 的组合数,其中 i[0,n]i \in[0,n]n=coins.lengthn= coins.lengthj[0,amount]j\in[0,amount]

💥NOTE,和法1相比,

  • dp[i][j]dp[i][j] 表示 [0,i)[0,i)(法1是 [0,i][0,i])区间凑成总和为 jj 的组合数;

  • ii 的取值范围变为 [0,n][0,n](法1是 i[0,n)i \in[0,n))。

2、确定 dp 状态方程

与法1相同,

  • 如果不选择 coins[i]coins[i],则有 dp[i][j]=dp[i1][j]dp[i][j]=dp[i-1][j]

  • 如果选择 coins[i]coins[i],则有 dp[i][j]+=dp[i1][jk×coins[i1]]dp[i][j] += dp[i-1][j - k \times coins[i - 1]]

其中,jk×coins[i]j \ge k \times coins[i]

3、确定 dp 初始状态

当背包容量恒为 00j=0j=0),从 [0,i)[0,i) 区间选择银币凑出总和为 00 的组合数只有 11 种(这个区间无硬币可选,那总和必然为 00),即 dp[0][0]=1dp[0][0] = 1

4、确定遍历顺序

  • 第一层遍历从 i=1i=1i=ni= n (第一层遍历的终点不同,法1是到 i=n1i=n-1

  • 第二层遍历从 j=1j=1j=amountj=amount

5、确定最终返回值

dp[n][amount]dp[n][amount]

6、代码示例

/**
 * 空间复杂度 O(amount * n),其中n是coins数组长度
 * 时间复杂度 O(amount * n)
 */
function change(amount: number, coins: number[]): number {
    const n = coins.length;
    const dp = Array.from({ length: n + 1 }, () => new Array(amount + 1).fill(0));

    dp[0][0] = 1;

    for (let i = 1; i <= n; i++) {
        const coin = coins[i - 1];
        for (let j = 0; j <= amount; j++) {
            dp[i][j] = dp[i - 1][j];
            for (let k = 1; k * coin <= j; k++) {
                dp[i][j] += dp[i - 1][j - k * coin];
            }
        }
    }

    return dp[n][amount];
};

状态压缩:此时定义 dp[j]dp[j] 为凑成总和为 j 的组合数,其实 j[0,amount]j \in [0, amount],且初始条件为凑成总和为 00 的组合数为 dp[0]dp[0],那就只有不选择任何银币,即 dp[0]=1dp[0]=1

/**
 * 空间复杂度 O(amount)
 * 时间复杂度 O(amount * coins.length)
 */
function change(amount: number, coins: number[]): number {

    const dp = new Array(amount + 1).fill(0);
    dp[0] = 1;

    for (const coin of coins) {
        for (let i = coin; i <= amount; i++) {
            dp[i] += dp[i - coin];
        }
    }
    return dp[amount];
};

参考

# 重识背包问题(上)

# 重识背包问题(下)