【力扣100题】49.分割等和子集

题目描述

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

示例:

  • 输入:nums = [1,5,11,5] → 输出:true(数组可以分割成 [1, 5, 5][11]
  • 输入:nums = [1,2,3,5] → 输出:false(数组不能分割成两个元素和相等的子集)

解题思路总览

方法 思路 时间复杂度 空间复杂度
动态规划(0-1背包) 转化为是否能从数组中选取若干元素使其和等于 sum/2 O(n × sum/2) O(sum/2)
DFS + 剪枝 从数组中尝试选取元素,看能否凑到目标值 O(2^n) O(n)
位运算优化 用 bitset 优化空间,适合 sum 较小的情况 O(n × sum/32) O(sum/32)

本题采用**动态规划(0-1背包)**方法。


核心思路转化

将原问题转化为:是否可以从数组中选取若干元素,使得它们的和等于 sum/2?

  • 如果能找到一个子集的和为 sum/2,那么剩下的元素和也是 sum/2,问题得解
  • 这就是一个「0-1背包」问题:每个数只能用一次,问能否装满容量为 sum/2 的背包

完整代码

cpp 复制代码
class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = 0;
        for (int i = 0; i < nums.size(); i++) sum += nums[i];
        if (sum % 2 == 1) return false;
        vector<int> dp(10001, 0);
        for (int i = 0; i < nums.size(); i++) {
            for (int j = sum / 2; j >= nums[i]; j--) {
                dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
            }
        }
        if (dp[sum / 2] == sum / 2) return true;
        return false;
    }
};

算法流程图

复制代码
输入: nums = [1, 5, 11, 5]

计算总和:
  sum = 1 + 5 + 11 + 5 = 22
  sum % 2 == 0? 是
  target = sum / 2 = 11

初始化:
  dp[0...11] = 0
  容量为 11 的背包

遍历数组:

i = 0, nums[0] = 1:
  j = 11: j >= 1, dp[11] = max(dp[11], dp[10]+1) = 1
  j = 10: j >= 1, dp[10] = max(dp[10], dp[9]+1) = 1
  ...
  j = 1: j >= 1, dp[1] = max(dp[1], dp[0]+1) = 1
  dp[1] = 1

i = 1, nums[1] = 5:
  j = 11: j >= 5, dp[11] = max(dp[11], dp[6]+5) = 6
  j = 10: j >= 5, dp[10] = max(dp[10], dp[5]+5) = 6
  ...
  j = 5: j >= 5, dp[5] = max(dp[5], dp[0]+5) = 5
  dp[5] = 5

i = 2, nums[2] = 11:
  j = 11: j >= 11, dp[11] = max(dp[11], dp[0]+11) = 11
  dp[11] = 11

i = 3, nums[3] = 5:
  j = 11: j >= 5, dp[11] = max(dp[11], dp[6]+5) = 11 (不更新)
  ...

最终 dp[11] = 11
dp[11] == target == 11? 是
返回 true

逐行解析

cpp 复制代码
int sum = 0;
for (int i = 0; i < nums.size(); i++) sum += nums[i];

含义: 计算数组所有元素的总和。

cpp 复制代码
if (sum % 2 == 1) return false;

含义: 如果总和是奇数,无法平分成两个和相等的子集,直接返回 false。

cpp 复制代码
vector<int> dp(10001, 0);

含义: 创建背包容量数组。dp[j] 表示容量为 j 的背包最多能装多少重量的物品(元素和)。这里 dp 大小设为 10001 是因为 sum <= 200 × 100 = 20000,所以 sum/2 <= 10000

cpp 复制代码
for (int i = 0; i < nums.size(); i++)

含义: 遍历数组中的每个元素(物品)。

cpp 复制代码
for (int j = sum / 2; j >= nums[i]; j--)

含义: 0-1 背包的关键:内层循环倒序遍历背包容量。这样可以保证每个元素只被使用一次(正序会导致完全背包问题)。

cpp 复制代码
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);

含义: 状态转移方程。对于当前元素 nums[i],要么不放入背包(保持 dp[j]),要么放入背包(dp[j - nums[i]] + nums[i])。取较大值更新。

cpp 复制代码
if (dp[sum / 2] == sum / 2) return true;

含义: 遍历结束后,如果 dp[sum/2] 恰好等于 sum/2,说明可以找到一个子集的和为 sum/2,返回 true。

cpp 复制代码
return false;

含义: 无法找到和为 sum/2 的子集,返回 false。


为什么内层循环要倒序?

复制代码
以 nums = [1, 5] 为例,target = 3

正序(错误):
  i=0, num=1: dp[1] = max(dp[1], dp[0]+1) = 1
            dp[2] = max(dp[2], dp[1]+1) = 2  <- 用到了本轮刚更新的 dp[1]
            dp[3] = max(dp[3], dp[2]+1) = 3  <- 用到了本轮刚更新的 dp[2]
  结果:num=1 被使用了多次!变成完全背包了

倒序(正确):
  i=0, num=1: dp[3] = max(dp[3], dp[2]+1)  dp[2] 还是旧值
            dp[2] = max(dp[2], dp[1]+1)  dp[1] 还是旧值
            dp[1] = max(dp[1], dp[0]+1) = 1
  结果:num=1 只被使用一次

复杂度分析

复杂度 说明
时间复杂度 O(n × sum/2) 外层循环 n 次,内层循环 sum/2 次
空间复杂度 O(sum/2) dp 数组大小为 sum/2 + 1

面试追问 FAQ

问题 答案
为什么能转化成 0-1 背包问题? 如果能找到和为 sum/2 的子集,剩下的元素和也是 sum/2,所以问题等价于「能否从数组中选若干元素使其和为 sum/2」
dp[j] 的含义是什么? 容量为 j 的背包最多能装多少重量的物品(即最多能达到多大的和)
为什么不直接用布尔数组 dp[j] 表示能否凑到 j? 因为 dp[j] 表示「最多」能装多少,可以用来判断是否恰好装满;布尔数组需要额外记录具体方案
dp[sum/2] == sum/2 为什么能判断「恰好装满」? dp[sum/2] 是背包最多能装到的重量,如果它等于 sum/2 说明可以恰好装满
进阶:如何输出具体的分割方案? 额外记录每个状态的选择路径,从 dp[sum/2] 开始回溯,找出被选中的元素
进阶:如何优化空间? 使用 bitset:`bitset<10001> bits; bits0 = 1; bits

相关题目

题号 题目 难度 核心思路
416 分割等和子集 中等 0-1 背包
322 零钱兑换 中等 完全背包
279 完全平方数 中等 动态规划
494 目标和 中等 0-1 背包(计数)
1049 最后一块石头的重量 II 中等 0-1 背包

总结

要点 内容
核心思想 将分割等和子集转化为 0-1 背包:能否从数组中选若干元素使其和为 sum/2
状态定义 dp[j] = 容量为 j 的背包最多能装的重量(元素和)
状态转移 dp[j] = max(dp[j], dp[j-nums[i]] + nums[i])
关键点 内层循环倒序,保证每个元素只使用一次
边界条件 sum 为奇数时直接返回 false
结果判断 dp[sum/2] == sum/2 表示恰好装满

相关推荐
dongf201914 小时前
R语言KNN算法
算法·数据分析·r语言
嵌入式ZYXC15 小时前
第2篇:《面试题:LDO和DC-DC的区别?分别用在什么场景?》
stm32·单片机·嵌入式硬件·面试·职场和发展
小O的算法实验室15 小时前
2025年IEEE TASE,基于双层耦合平均场博弈的大规模智能体集成任务分配与轨迹规划
人工智能·算法·机器学习
8Qi815 小时前
LeetCode 337:打家劫舍 III(House Robber III)—— 题解 ✅
算法·leetcode·二叉树·动态规划
地平线开发者15 小时前
从 INT64 Div 算子约束到 Cast 修复全流程
算法
2601_9611940215 小时前
教资科三美术考什么|初中高中美术题型考点和模板资料
leetcode·elasticsearch·职场和发展·蓝桥杯·pat考试·lucene
AI科技星15 小时前
基于奇合数边界的离散解析数论与双螺旋宇宙本体大统一体系论文全部数学公式汇总表
人工智能·算法·机器学习·架构·学习方法
地平线开发者15 小时前
Horizon 模型多 Batch 配置
算法·自动驾驶
我命由我1234515 小时前
工程中安全帽颜色含义
运维·经验分享·学习·职场和发展·求职招聘·职场发展·学习方法
czhaii15 小时前
GB2312简体中文编码表
单片机·算法