416. 分割等和子集(partition-equal-subset-sum)

3,772 阅读2分钟

"你的背包,不再让我走得缓慢"

416. 分割等和子集:一个 只包含正整数非空(至少有一个元素) 数组 numsnums。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。提示:1<=nums.length<=2001 <= nums.length <= 2001<=nums[i]<=1001 <= nums[i] <= 100

示例1示例2
输入: nums=[1,5,11,5]nums = [1,5,11,5]
输出: truetrue
解释: 数组可以分割成 [1,5,5][1, 5, 5][11][11]
输入: nums=[1,2,3,5]nums = [1,2,3,5]
输出: falsefalse
解释: 数组不能分割成两个元素和相等的子集。

中规中矩的 0-1 背包

从整体分析,题意要求的两个元素和相等的子集,不是子数组,因此不需要元素是连续的。

我们将这道题转换成 010-1 背包问题(参考 重识背包问题(上)),即

  • 每个元素不能重复使用

  • 背包体积为 12×i=0n1nums[i]\frac{1}{2} \times \sum_{i=0}^{n-1} nums[i] (💥前置结论:如果 numsnums 内所有元素之和为奇数,一定不能分割成两个元素和相等的子集)

  • 背包中第 ii 件物品的重量与价值均为 nums[i]nums[i]

  • 如果背包正好装满,说明可以分割成两个元素和相等的子集

1、确定 dp 数组及含义

💥 dp[i][j]dp[i][j] 代表从 [0,i][0, i] 中取任意物品放进容量为 jj 的背包后的物品总价值,其中 i[0,nums.length)i \in [0, nums.length)j[0,W],W=12×i=0n1nums[i]j \in [0, W], W = \frac{1}{2} \times \sum_{i=0}^{n-1} nums[i]

2、确定 dp 对应的状态方程

对于第 ii 件物品,有两种状态,即放入背包不放入背包

  • 主动不放入: 这种情况不受限于背包的容量,即 dp[i][j]=dp[i1][j]dp[i][j] = dp[i-1][j]

  • 被动不放入: 这种情况下受限于背包的容量,不能再将 nums[i]nums[i] 的物品放入背包,此时 j<nums[i]j<nums[i],那么状态转移方程就是 dp[i][j]=dp[i1][j]dp[i][j] = dp[i-1][j]

  • 主动放入: 此时 jnums[i]j \ge nums[i],要先从背包里腾出这个空间,再将 nums[i]nums[i] 的物品放入背包,即 dp[i][j]=dp[i1][jnums[i]]+nums[i]dp[i][j] = dp[i-1][j-nums[i]] + nums[i]

综上所述,令 σ=jnums[i]\sigma =j - nums[i]

dp[i][j]={dp[i1][j]σ<0max(dp[i1][j],dp[i1][σ]+value[i])σ0dp[i][j] = \begin{cases} dp[i-1][j] & \sigma < 0 \\ max(dp[i-1][j], dp[i-1][\sigma] + value[i]) & \sigma \ge0 \end{cases}

3、确定 dp 初始状态

首先,当背包的承载为 00,总价值必定为 00;其次其次,只有第一件物品时,只要当前背包容量存在,jnums[0]j \ge nums[0],最大价值即为 nums[0]nums[0],否则是 00

4、确定遍历顺序

标准二维 dpdp 的背包遍历顺序:

  • 第一层循环:ii00nums.length1nums.length - 1
  • 第二层循环:jj00WW

5、确定返回值

当从 [0,N)[0,N) 中选取物品,放入容里为 WW 的背包后,总价值恰为 WW 时,即 dp[N1][W]==Wdp[N-1][W] == W,说明当前数组 numsnums 可以分为两个和一样的子数组。

6、示例代码

二维 dpdp

/**
 * 时间复杂度:O(n*target), 其中 n 是数组的长度, target 是整个数组的元素和的一半。
 * 空间复杂度:O(n*target)
 */
function canPartition(nums: number[]): boolean {
    const sum = nums.reduce((p, c) => p + c, 0); // 元素总和

    if (sum & 1) { // 求奇偶性
        return false;
    }

    const bagWeight = sum >> 1;
    const length = nums.length;

    const dp = Array.from({ length }, () => new Array(bagWeight + 1).fill(0));

    for(let j = nums[0]; j <= bagWeight; j++) {
        dp[0][j] = nums[0]; 
    }

    for(let i = 1; i < length; i++) {
        for(let j = 1; j <= bagWeight; j++) {
            if (j < nums[i]) {
                dp[i][j] = dp[i - 1][j];
            } else {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - nums[i]] + nums[i]);
            }
        }
    }

    return dp[length - 1][bagWeight] === bagWeight;
};

状态压缩:💥此时 dp[j]dp[j] 表示容量为 jj 的背包所承载的最大价值。dp[0]dp[0] 能初始化为 00 的原因是,numsnums 中每个元素均为正整数,局部最小值可以取为 00

/**
 * 时间复杂度:O(n*target), 其中 n 是数组的长度, target 是整个数组的元素和的一半。
 * 空间复杂度:O(target)
 */
function canPartition(nums: number[]): boolean {
    const sum = nums.reduce((p, c) => p + c, 0);

    if (sum & 1) {
        return false;
    }

    const bagWeight = sum >> 1;
    const length = nums.length;

    const dp = new Array(bagWeight + 1).fill(0);

    for(let i = 1; i < length; i++) {
        // 每个元素一定不能重复放入背包内,故要倒序遍历
        for(let j = bagWeight; j >= nums[i]; j--) {
                dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
        }
    }

    return dp[bagWeight] === bagWeight;
};