416. 分割等和子集

0 阅读3分钟

给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

示例 1:

输入: nums = [1,5,11,5]
输出: true
解释: 数组可以分割成 [1, 5, 5][11]

示例 2:

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

提示:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 100

🏠 生活案例:公平的遗产分配

想象你和你的兄弟继承了一堆金条,每根金条的重量不同(比如 [1, 5, 11, 5])。

你们想把这堆金条分成完全相等的两份,一人一半。

逻辑转化:

  1. 首先,算一下所有金条的总重量(Sum)。如果总重量是奇数(比如 21斤),那肯定没法平分,直接放弃。
  2. 如果总重量是偶数(比如 22斤),那么你们的目标就是:能不能从这堆金条里挑出几根,凑够总重量的一半(Target = 11斤)? * 如果你能凑出 11 斤,剩下的那堆自然也是 11 斤。

💻 代码实现与生活化注释

这段代码使用了动态规划 (DP) 。它维护一个布尔数组 dp,用来记录“当前的重量是否可以被凑出来”。

JavaScript

/**
 * @param {number[]} nums
 * @return {boolean}
 */
var canPartition = function (nums) {
    // 1. 先算总重量
    let sum = nums.reduce((a, b) => a + b, 0);

    // 2. 如果总重量是奇数,直接返回 false(无法平分)
    if (sum % 2 !== 0) return false;

    // 3. 目标重量:我们要凑出总和的一半
    let target = sum / 2;

    // 4. 创建一个记账本 dp,长度为 target + 1
    // dp[j] 表示:利用手头的金条,是否能凑出重量 j?
    let dp = new Array(target + 1).fill(false);

    // 初始状态:重量为 0 永远是可以凑出来的(一根金条都不拿)
    dp[0] = true;

    // 5. 遍历每一根金条 (num)
    for (let num of nums) {
        // 6. 尝试用这根金条去更新我们的“可凑出重量清单”
        // 注意:这里要从后往前遍历(target 到 num),
        // 这是为了保证每根金条在当前轮次只被使用一次(0-1背包的核心)
        for (let j = target; j >= num; j--) {
            // 如果已经凑到了 target,提前收工
            if (dp[target]) return true;

            // 核心逻辑:
            // 到达重量 j 有两种可能:
            // 1. 我本来就能凑出 j (dp[j] 为 true)
            // 2. 我以前能凑出 j - num,现在加上这根金条 num,就能凑出 j 了
            dp[j] = dp[j] || dp[j - num];
        }
    }

    // 7. 最后看目标重量 target 的格子是不是 true
    return dp[target];
};

🧩 为什么循环要“从后往前”?(重点)

这是 0-1 背包问题的精髓。

想象你在填一张表。如果你从前往后填,比如你现在有一根 2 斤的金条:

  • 你看到 dp[0] 是真,于是把 dp[2] 变真。
  • 接着你往后走,看到 dp[2] 是真,你可能会把 dp[4] 也变真。
  • 这相当于你把同一根金条用了两次!

从后往前走,当你更新 dp[target] 时,参考的是上一轮(还没用这根金条时)的小重量数据,这样就保证了每根金条只会被“捡起来”放入包裹一次。


💡 算法复杂度

  • 时间复杂度O(n×target)O(n \times \text{target})。其中 nn 是金条的数量,target\text{target} 是总和的一半。
  • 空间复杂度O(target)O(\text{target})。我们只用了一个一维数组来滚动记录。

这个方法比暴力搜索(尝试所有组合)要快得多,因为它避免了大量的重复计算。