题目解析
题目:给定一个正整数数组 nums,判断是否可以将数组分成两个和相等的子集。
等价问题:
• 计算 nums 的总和 S
• 如果 S 是奇数,直接返回 false(因为不能均分)
• 目标是找到一个子集,使得子集和等于 S / 2
• 这相当于 0-1 背包问题:是否能从 nums 中选取一些数,使其和为 S / 2
解法:动态规划
1. 状态定义
使用 一维 DP 数组 dp[j]:
• dp[j] = true 表示存在一个子集,使得该子集的和为 j
• dp[j] = false 表示不存在这样的子集
目标:判断 dp[target] 是否为 true,其中 target = S / 2
2. 递推公式
• 遍历数组 nums,对于当前元素 num
• 倒序遍历 j = target 到 num,更新 dp[j]
• dp[j] = dp[j] || dp[j - num]
• 如果 dp[j - num] 为 true,说明 j - num 可达,那么 j 也可达
• 如果 dp[j] 原本为 true,则仍为 true
3. 初始化
• dp[0] = true(空集的和为 0)
代码分析
class Solution {
public:
bool canPartition(vector<int>& nums) {
int s = accumulate(nums.begin(), nums.end(), 0); // 计算总和
if (s % 2 != 0) return false; // 总和为奇数,无法均分
int target = s / 2; // 目标和
vector<bool> dp(target + 1, false); // DP 数组,表示是否能组成某个和
dp[0] = true; // 0 直接可达
for (int num : nums) { // 遍历每个数
for (int j = target; j >= num; j--) { // 逆序遍历
dp[j] = dp[j] || dp[j - num];
}
}
return dp[target]; // 看 target 是否可达
}
};
详细运行步骤
示例 1
输入:
nums = {1, 5, 11, 5}
计算总和:
• S = 1 + 5 + 11 + 5 = 22
• target = 22 / 2 = 11
• 需要找到一个子集和为 11
DP 过程:
初始化:
dp = [true, false, false, false, false, false, false, false, false, false, false, false]
(下标 0 代表和为 0 可达)
遍历 1:
dp[1] = dp[1] || dp[1 - 1] (true)
dp = [true, true, false, false, false, false, false, false, false, false, false, false]
遍历 5:
dp[6] = dp[6] || dp[6 - 5] (true)
dp[5] = dp[5] || dp[5 - 5] (true)
dp = [true, true, false, false, false, true, true, false, false, false, false, false]
遍历 11:
dp[11] = dp[11] || dp[11 - 11] (true)
dp = [true, true, false, false, false, true, true, false, false, false, false, true]
最终 dp[11] = true,返回 true
案例分析
我们用一个不同的测试案例,详细跟踪 dp 数组的变化。
示例 2
输入
nums = {3, 3, 3, 4, 5}
计算总和:
• S = 3 + 3 + 3 + 4 + 5 = 18
• target = 18 / 2 = 9
• 目标是找到一个子集,使得该子集的和为 9
初始化
我们初始化 dp 数组:
dp = [true, false, false, false, false, false, false, false, false, false]
• dp[0] = true,表示和为 0 的子集是可行的(空集)
遍历每个数
遍历 3(第一个)
更新 dp[j],从 target = 9 逆序到 num = 3
dp[3] = dp[3] || dp[3 - 3] = true
dp 变化:
dp = [true, false, false, true, false, false, false, false, false, false]
表示可以用 {3} 得到和 3
遍历 3(第二个)
dp[6] = dp[6] || dp[6 - 3] = true
dp[3] = dp[3] || dp[3 - 3] = true (已经是 true)
dp 变化:
dp = [true, false, false, true, false, false, true, false, false, false]
表示可以用 {3, 3} 得到和 6
遍历 3(第三个)
dp[9] = dp[9] || dp[9 - 3] = true
dp[6] = dp[6] || dp[6 - 3] = true (已经是 true)
dp[3] = dp[3] || dp[3 - 3] = true (已经是 true)
dp 变化:
dp = [true, false, false, true, false, false, true, false, false, true]
表示可以用 {3, 3, 3} 得到和 9 ✅ 目标达成
遍历 4
dp[9] = dp[9] || dp[9 - 4] = true (已经是 true)
dp[7] = dp[7] || dp[7 - 4] = true
dp[4] = dp[4] || dp[4 - 4] = true
dp 变化:
dp = [true, false, false, true, true, false, true, true, false, true]
表示可以用 {4} 得到和 4,可以用 {3, 4} 得到 7
遍历 5
dp[9] = dp[9] || dp[9 - 5] = true
dp[8] = dp[8] || dp[8 - 5] = true
dp[5] = dp[5] || dp[5 - 5] = true
dp 变化:
dp = [true, false, false, true, true, true, true, true, true, true]
可以用 {5} 得到 5,用 {3, 5} 得到 8
最终结果
✅ dp[9] = true,说明可以找到一个子集使得总和为 9,返回 true
完整执行过程
dp 状态 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
初始 | T | F | F | F | F | F | F | F | F | F |
处理 3 | T | F | F | T | F | F | F | F | F | F |
处理 3 | T | F | F | T | F | F | T | F | F | F |
处理 3 | T | F | F | T | F | F | T | F | F | T |
处理 4 | T | F | F | T | T | F | T | T | F | T |
处理 5 | T | F | F | T | T | T | T | T | T | T |
dp[9] = true,所以可以划分两个子集和相等。
复杂度分析
• 时间复杂度:O(n × target) 其中 target = S/2
• 空间复杂度:O(target) 只用了一维 dp
总结
✅ 思路:
1. 计算 S,如果 S 为奇数直接返回 false
2. 定义 dp[j]:表示能否找到一个子集,使得总和为 j
3. 遍历 nums,使用 逆序 更新 dp,确保不会重复使用数字
4. 最终 dp[target] 是否为 true,决定是否能划分
✅ 关键优化点:
• 使用 一维 DP 数组(空间优化)
• 倒序遍历 j,避免同一轮重复使用 nums[i]
✅ 时间复杂度:
• O(n × sum/2),适用于 sum 适中的情况(sum < 10^5)
✅ 空间复杂度:
• O(sum/2),降低空间占用