416. 分割等和子集

问题分析
题目要求判断是否可以将一个只包含正整数的数组 nums 分割成两个子集,使得这两个子集的元素和相等。这是一个经典的 0-1 背包问题 的变种,可以通过动态规划(DP)来解决。
思路
总和为奇数时无法分割:
如果数组的总和是奇数,那么不可能将其分成两个和相等的子集,直接返回 False。
目标值:
如果总和是偶数,设总和为 total,则每个子集的和应该为 target = total // 2。
问题转化为:是否存在一个子集,其元素和等于 target。
动态规划思路:
定义一个布尔型 DP 数组 dp,其中 dp[j] 表示是否存在一个子集,其元素和等于 j。
初始化 dp[0] = True,表示和为 0 的子集总是存在(空集)。
遍历数组中的每个元素,更新 DP 数组:
对于当前元素 num,从大到小遍历 dp 数组,更新 dp[j] = dp[j] or dp[j - num]。
这样可以避免重复使用同一个元素。
代码
class Solution:
def canPartition(self, nums: List[int]) -> bool:
# 计算数组总和
total_sum = sum(nums)
# 如果总和是奇数,无法分割成两个和相等的子集
if total_sum % 2 != 0:
return False
# 目标值为总和的一半
target = total_sum // 2
# 初始化 DP 数组,dp[j] 表示是否存在子集和为 j
dp = [False] * (target + 1)
dp[0] = True # 和为 0 的子集总是存在(空集)
# 遍历数组中的每个元素
for num in nums:
# 从大到小遍历 DP 数组,避免重复使用同一个元素
for j in range(target, num - 1, -1):
dp[j] = dp[j] or dp[j - num]
# 返回目标值是否可达
return dp[target]
复杂度分析
时间复杂度:
遍历数组中的每个元素,对于每个元素,需要遍历 dp 数组一次,因此总时间为 (O(n * target)),其中 (n) 是数组长度,(target) 是目标和。
空间复杂度:
使用了一个长度为 target + 1 的 DP 数组,因此空间复杂度为 (O(target))。
我的学习
0-1 背包问题,考虑 动态规划
两个子集的元素和相等,意味着:
把 nums 分成两个子集,每个子集的元素和恰好等于s /2,s 必须是偶数。
如果 s 是奇数, s /2不是整数,直接返回 false。
动态规划:
dp[j] = dp[j] or dp[j - num]
dp[j] 的含义
dp[j] 表示是否存在一个子集,其元素和等于 j。
初始化时,dp[0] = True,表示和为 0 的子集总是存在(空集)。
2. dp[j - num] 的含义
dp[j - num] 表示是否存在一个子集,其元素和等于 j - num。
如果 dp[j - num] 为 True,说明存在一个子集,其元素和为 j - num。那么,加上当前元素 num 后,子集的总和变为 j。
3. dp[j] = dp[j] or dp[j - num] 的作用
这一行代码的作用是更新 dp[j]:
如果 dp[j] 已经为 True,说明之前已经找到了一个子集,其元素和为 j,此时无需更改。
如果 dp[j] 为 False,但 dp[j - num] 为 True,说明可以通过加入当前元素 num 来构造一个和为 j 的子集,因此将 dp[j] 设置为 True。
为什么要从大到小遍历:
在更新 dp[j] 时,必须确保每个元素只使用一次。如果从小到大遍历 dp 数组,可能会重复使用同一个元素。
例如,假设数组中有两个相同的元素 num,如果从小到大遍历,dp[j] 可能会被错误地更新为 True,因为同一个元素被多次使用。
因此,从大到小遍历可以避免重复使用同一个元素。
1426

被折叠的 条评论
为什么被折叠?



