给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入: nums = [1,5,11,5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入: nums = [1,2,3,5]
输出: false
解释: 数组不能分割成两个元素和相等的子集。
提示:
1 <= nums.length <= 2001 <= nums[i] <= 100
🏠 生活案例:公平的遗产分配
想象你和你的兄弟继承了一堆金条,每根金条的重量不同(比如 [1, 5, 11, 5])。
你们想把这堆金条分成完全相等的两份,一人一半。
逻辑转化:
- 首先,算一下所有金条的总重量(Sum)。如果总重量是奇数(比如 21斤),那肯定没法平分,直接放弃。
- 如果总重量是偶数(比如 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] 时,参考的是上一轮(还没用这根金条时)的小重量数据,这样就保证了每根金条只会被“捡起来”放入包裹一次。
💡 算法复杂度
- 时间复杂度:。其中 是金条的数量, 是总和的一半。
- 空间复杂度:。我们只用了一个一维数组来滚动记录。
这个方法比暴力搜索(尝试所有组合)要快得多,因为它避免了大量的重复计算。