返回
算法必刷题系列[93]: 分割等和子集 (partition-equal-subset-sum)
前端
2023-12-13 12:59:03
在算法必刷题系列中,我们已经学习了多种数据结构和算法。今天,我们来学习一道经典的 0-1 背包问题:分割等和子集 。
题目
给你一个只包含正整数的数组 nums。判断是否存在一个子集,使得子集和为目标和 target 的一半。
示例 1:
输入:nums = [1, 5, 11, 5], target = 11
输出:true
解释:我们可以选择 nums 中的子集 [1, 5, 5],和为 11,恰好等于目标和的一半。
示例 2:
输入:nums = [1, 2, 3, 5], target = 11
输出:false
解释:不存在 nums 中的子集和为 11 的一半。
提示:
- 1 <= nums.length <= 200
- 1 <= nums[i] <= 1000
- 1 <= target <= 1000
解题思路
这道题可以用动态规划或回溯算法求解。
动态规划
状态定义:
dp[i][j] 表示考虑前 i 个元素,能否凑出和为 j 的子集
状态转移方程:
dp[i][j] = dp[i-1][j] || dp[i-1][j - nums[i]]
初始化:
dp[0][0] = true
具体步骤:
- 外层循环遍历所有元素,内层循环遍历所有可能的和。
- 如果不考虑当前元素,即
dp[i-1][j]
为 true,则dp[i][j]
也为 true。 - 如果考虑当前元素,即
dp[i-1][j - nums[i]]
为 true,则dp[i][j]
也为 true。 - 如果以上两个条件都不满足,则
dp[i][j]
为 false。 - 最后,判断
dp[n][target/2]
是否为 true 即可。
回溯算法
回溯函数:
def backtrack(i, cur_sum):
if i == n:
return cur_sum == target // 2
# 考虑当前元素
if backtrack(i + 1, cur_sum + nums[i]):
return True
# 不考虑当前元素
if backtrack(i + 1, cur_sum):
return True
return False
具体步骤:
- 定义回溯函数
backtrack
,其中 i 表示当前考虑的元素索引,cur_sum 表示当前子集的和。 - 如果 i 等于数组长度 n,表示已经考虑完所有元素,此时判断
cur_sum
是否等于目标和的一半即可。 - 如果考虑当前元素,则递归调用
backtrack
函数,并将cur_sum
加上当前元素的值。 - 如果不考虑当前元素,则递归调用
backtrack
函数,保持cur_sum
不变。 - 如果以上两个条件都不满足,则表示不存在符合条件的子集,返回 False。
- 最后,调用
backtrack(0, 0)
即可得到结果。
时间复杂度
- 动态规划:O(n * target)
- 回溯算法:O(2^n)
空间复杂度
- 动态规划:O(n * target)
- 回溯算法:O(n)
代码实现
动态规划
def canPartition(nums, target):
n = len(nums)
dp = [[False] * (target + 1) for _ in range(n + 1)]
dp[0][0] = True
for i in range(1, n + 1):
for j in range(target + 1):
dp[i][j] = dp[i-1][j]
if j - nums[i] >= 0:
dp[i][j] |= dp[i-1][j - nums[i]]
return dp[n][target // 2]
回溯算法
def canPartition(nums, target):
n = len(nums)
def backtrack(i, cur_sum):
if i == n:
return cur_sum == target // 2
# 考虑当前元素
if backtrack(i + 1, cur_sum + nums[i]):
return True
# 不考虑当前元素
if backtrack(i + 1, cur_sum):
return True
return False
return backtrack(0, 0)