【力扣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; bits[0] = 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 表示恰好装满

相关推荐
瑞行AI1 小时前
一套数据格式框架搞定大模型微调和对齐训练
算法·语言模型
玛卡巴卡ldf1 小时前
【LeetCode 手撕算法】(动态规划)爬楼梯、杨辉三角、打家劫舍、完全平方数、零钱兑换、单词拆分、最长递增子序列、乘积最大子数组、分割等和子集
java·数据结构·算法·leetcode·动态规划·力扣
jake·tang1 小时前
深度解析 VESC 参数辨识源码:电阻、电感与磁链
arm开发·c++·嵌入式硬件·算法·数学建模·傅立叶分析
图码1 小时前
矩阵边界遍历:顺时针与图案打印的两种高效解法
数据结构·python·线性代数·算法·青少年编程·矩阵·深度优先遍历
sali-tec1 小时前
C# 基于OpenCv的视觉工作流-章72-点-点距离
图像处理·人工智能·opencv·算法·计算机视觉
凌波粒1 小时前
LeetCode--二叉树层序遍历实战指南
算法·leetcode·职场和发展
洛水水1 小时前
【力扣100题】48.乘积最大子数组
算法·leetcode·职场和发展
小小de风呀1 小时前
de风——【从零开始学C++】(七):string类详解
开发语言·c++·算法
YL200404261 小时前
042将有序数组转换为二叉搜索树
数据结构·算法·leetcode