算法416. 分割等和子集
题目
给你一个 只包含正整数 的 非空 数组 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
题解
class Solution:def canPartition(self, nums: List[int]) -> bool:memo={}# dfs的目的是考虑前 i+1 个数(即 nums[0] 到 nums[i])的情况下,能否选出一个子集,使其元素之和恰好等于jdef dfs(i:int,j:int) -> bool:if (i,j) in memo:return memo[(i,j)]# 结束条件,i遍历完if i<0:return j==0# 背包装不下,或者可以理解成所有值得和大于0,所以不存在j-nums[j]<0的情况# 剪枝if j<nums[i]:memo[(i,j)] = dfs(i-1,j)return dfs(i-1,j) # 不选择该值memo[(i,j)] = dfs(i-1,j-nums[i]) or dfs(i-1,j)return dfs(i-1,j-nums[i]) or dfs(i-1,j)s=sum(nums)return s%2==0 and dfs(len(nums) - 1, s // 2)
这里可以使用0-1背包的思路思考这道题
🎯 一、问题转化:从“分割数组”到“0-1 背包”
题目:给定一个只包含正整数的非空数组 nums
,判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
✅ 转化步骤:
- 计算总和:
s = sum(nums)
- 如果
s
是奇数 → 不可能平分 → 直接返回False
- 如果
s
是偶数 → 目标变成:能否从nums
中选出若干个数,使其和恰好等于target = s // 2
?
🔑 这就是经典的 0-1 背包问题中的“恰好装满”判定问题!
🎒 二、0-1 背包的标准模型回顾
背包要素 | 本题对应内容 |
---|---|
物品 | 数组中的每个数 nums[i] |
物品重量 | nums[i] (同时也是“价值”,但这里不关心价值) |
背包容量 | target = s // 2 |
选择限制 | 每个数最多选一次(0-1) |
目标 | 能否恰好装满背包(即子集和 = target) |
💡 注意:这里我们不求最大价值,而是判断是否存在一种选法,使总重量恰好等于容量。
🧠 三、状态定义(DP / DFS)
递归函数定义(记忆化搜索版):
def dfs(i, j) -> bool:
- 含义:考虑前
i+1
个物品(即nums[0..i]
),能否选出一个子集,使其和恰好为j
。 - 目标:
dfs(n-1, target)
这完全对应 0-1 背包的状态定义:
dp[i][j] = 能否用前 i 个物品凑出重量 j
🔁 四、状态转移方程(0-1 背包核心)
对于第 i
个物品(重量 = nums[i]
),有两种选择:
情况 1️⃣:不选第 i
个物品
- 那么问题变成:用前
i-1
个物品凑出j
- 对应:
dfs(i - 1, j)
情况 2️⃣:选第 i
个物品(前提是能装下!)
- 条件:
j >= nums[i]
- 那么问题变成:用前
i-1
个物品凑出j - nums[i]
- 对应:
dfs(i - 1, j - nums[i])
✅ 状态转移方程:
if j >= nums[i]:dfs(i, j) = dfs(i-1, j - nums[i]) or dfs(i-1, j)
else:dfs(i, j) = dfs(i-1, j) # 只能不选
这正是你代码中的逻辑!
🛑 五、边界条件(Base Case)
if i < 0: # 没有物品可选了return j == 0
- 当没有物品时(
i = -1
):- 如果目标
j == 0
→ 空集满足条件 →True
- 如果
j > 0
→ 无法凑出正数 →False
- 如果目标
这对应背包问题中“用 0 个物品凑出 0 重量是可行的,凑出正重量不可行”。
🧪 六、举个完整例子(按背包逻辑走一遍)
nums = [1, 5, 11, 5]
s = 22
→target = 11
- 问:能否从
[1,5,11,5]
中选出若干数,和为 11? - 物品:4 个,重量分别是 1, 5, 11, 5
- 背包容量:11
尝试选法:
- 选
11
→ 刚好凑满 ✅ - 或选
1 + 5 + 5 = 11
✅
所以 dfs(3, 11)
应返回 True
递归过程会探索:
- 在
i=3
(值为 5)时,j=11 >= 5
→ 尝试选或不选- 选:看
dfs(2, 6)
- 不选:看
dfs(2, 11)
- 选:看
dfs(2, 11)
中,nums[2]=11
,j=11 >= 11
→ 选它 →dfs(1, 0)
→ 最终i=-1, j=0
→True
🧩 七、为什么这是“0-1 背包”而不是“完全背包”?
- 0-1 背包:每个物品只能用一次 → 对应“每个数只能选一次”
- 完全背包:物品可用无限次 → 不符合题意(不能重复使用同一个元素)
所以本题是标准的 0-1 背包判定问题。
📌 八、总结:0-1 背包视角下的完整逻辑链
步骤 | 内容 |
---|---|
1. 问题转化 | 分割等和子集 ⇨ 能否选出子集和 = sum(nums)//2 |
2. 模型识别 | 物品 = 数字,重量 = 数值,容量 = target,选或不选 → 0-1 背包 |
3. 状态定义 | dfs(i, j) = 前 i+1 个数能否凑出和 j |
4. 状态转移 | 能装下就“选或不选”,装不下就“只能不选” |
5. 边界条件 | 无物品时,只有 j=0 可行 |
6. 最终答案 | dfs(n-1, target) |
✅ 结论:
canPartition
问题本质上就是一个 0-1 背包的“可行性判定”变种,完全符合 0-1 背包的逻辑框架。
代码正是用记忆化搜索实现的 0-1 背包解法,非常标准!