377. 组合总和 Ⅳ (combination sum iv)

4,031 阅读3分钟

"又见背包,让我不再走得缓慢"

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

377. 组合总和 Ⅳ 题目描述:给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。

示例
输入nums=[1,2,3]nums = [1,2,3], target=4target = 4
输出77
解释:所有可能的组合为:(1, 1, 1, 1)(1, 1, 2)(1, 2, 1)(1, 3)(2, 1, 1)(2, 2)(3, 1)note:note: 请注意,顺序不同的序列被视作不同的组合。

乍一看本题和 # 518. 零钱兑换 II 如出一辙,但是细看示例才发现,题目提到的“组合”个数其实是“排列”个数,所以这道题其实是 完全背包 问题下的 排列 问题。

中规中矩的动态规划

1、确定 dp 状态数组

定义 dp[i][j]dp[i][j][0,i)[0,i) 区间内选择元素时,凑成目标整数为 jj 的排列个数,其中 i[0,n]i \in [0, n]n=nums.lengthn=nums.lengthj[0,target]j \in [0, target]

2、确定 dp 状态方程

放弃 nums[i1]nums[i - 1] 元素时,则有 dp[i][j]=dp[i1][j]dp[i][j] = dp[i - 1][j],此时 j<nums[i1]j \lt nums[i - 1]

选择 nums[i1]nums[i - 1] 元素时,则有 dp[i][j]=dp[i1][j]+dp[n][jnums[i]]dp[i][j] = dp[i - 1][j] + dp[n][j - nums[i]],此时 jnums[i1]j \ge nums[i - 1]

NOTE:

  1. 为何是 nums[i1]nums[i - 1] 而不是 nums[i]nums[i]?因为 ii 方向上有一个“哨兵”,即 dpdpii 方向上比 numsnums 多了一个元素。

  2. 为何是 dp[n][jnums[i]]dp[n][j - nums[i]] 而不是 dp[i1][jnums[i]]dp[i- 1][j - nums[i]]?因为这是 排列 问题,dp[i1][jnums[i]]dp[i- 1][j - nums[i]] 计算出来的依然是组合个数,而非排列。

3、确定 dp 初始状态

对于任意 i[0,n]i \in [0, n],均存在 dp[i][0]=1dp[i][0] = 1,即背包容量为 00,总有一种选择方法(什么物品都不选)。

4、确定遍历顺序

完全背包的排列问题,先遍历背包,再遍历物品,故

  • 外循环,从 j=1j = 1 遍历到 j=targetj = target

  • 内循环,从 i=1i = 1 遍历到 i=ni = n

5、确定最终返回值

依然要回归到 dpdp 状态定义中,即 dp[n][target]dp[n][target][0,n)[0,n) 区间内选择元素时,凑成目标整数为 jj 的排列个数。

6、代码示例

/**
 * 空间复杂度 O(target * nums.length)
 * 时间复杂度 O(target * nums.length)
 */
function combinationSum4(nums: number[], target: number): number {
    const n = nums.length;
    const dp = Array.from({ length: n + 1}, () => new Array(target + 1).fill(0));

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

    for (let j = 1; j <= target; j++) {
        for (let i = 1; i <= n; i++) {
            if (j - nums[i - 1] < 0) {
                dp[i][j] = dp[i - 1][j];
            } else {
                dp[i][j] = dp[i - 1][j] + dp[n][j - nums[i - 1]];
            }
        }
    }

    return dp[n][target];
};

思维提升-空间压缩

1、确定 dp 状态数组

定义 dp[j]dp[j] 是凑成目标整数为 jj 的排列个数,其中 j[0,target]j \in [0, target]

2、确定 dp 状态方程

完全背包问题一维 dpdp 模型(参考 # 重识背包问题(下))应为,

dp[j]+=dp[jnums[i]]dp[j] += dp[j - nums[i]]

其中,

  • i[0,n),n=nums.lengthi \in [0, n),n = nums.length

  • jnums[i]0j - nums[i] \ge 0

🎡 NOTE: 当 jnums[i]<0j - nums[i] \lt 0 时,继续循环即可,无须额外计算。

3、确定 dp 初始状态

当目标整数为 00 时,即 j=0j = 0,能凑成 00 的排列个数一定为 11,即不选择任何元素。(因为题目规定了任意 nums[i]1nums[i] \ge 1,所以这里才能直接赋值为 11)。

4、确定遍历顺序

完全背包的排列问题,必须先遍历背包,再遍历物品,故

  • 外循环,从 j=1j = 1 遍历到 j=targetj = target

  • 内循环,从 i=0i = 0 遍历到 i=n1i = n-1

5、确定最终返回值

依然要回归到 dpdp 状态定义中,即 dp[target]dp[target] 是凑成 targettarget 的排列个数。

6、代码示例

/**
 * 空间复杂度 O(target)
 * 时间复杂度 O(target * nums.length)
 */
function combinationSum4(nums: number[], target: number): number {
    const n = nums.length;
    const dp = new Array(target + 1).fill(0);

    dp[0] = 1;

    for (let j = 1; j <= target; j++) {
        for (let i = 0; i < n; i++) {
            if (j - nums[i] >= 0) {
                dp[j] += dp[j - nums[i]];
            }
        }
    }

    return dp[target];
};

参考

# 重识背包问题(上)

# 重识背包问题(下)