一.题目描述
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意:
每个数组中的元素不会超过 100
数组的大小不会超过 200
示例 1:
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
示例 2:
输入: [1, 2, 3, 5]
输出: false
解释: 数组不能分割成两个元素和相等的子集.
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/partition-equal-subset-sum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
二.思路分析
既然可以分解成两个相等的子集,那么只需要找到一个子集的和等于总的集合的和的一半就好了,也就是在给定的集合中找一个子集满足特定的和。
如果用暴力搜索的话,是列出所有的子集,一共有2^n种,每个子集对应的是是否包含每个元素,然后看看是否有子集的和等于给定的值,也就是0-1背包问题
对于这种暴力搜索时每个元素在与不在对应一个子集,动态规划一般是考虑在原来元素对应状态的基础上考虑这个元素在与不在两种情况,比如这道题的递推式为:
dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i]], (nums[i] <= j)
上面的dp[i][j]表示从nums[0]到nums[i]挑选元素看使得它们的和等于j,
可以有两种情况:
(1)不包含nums[i],这时候只需要num[0]-nums[i-1]中挑选和等于j的元素子集
(2)包含nums[i],这时候只需要num[0]-nums[i-1]中挑选和等于j-nums[i]的元素子集
代码如下:
from typing import List
class Solution:
def canPartition(self, nums: List[int]) -> bool:
size = len(nums)
s = sum(nums)
if s & 1 == 1:
return False
target = s // 2
dp = [False for _ in range(target + 1)]
# 状态转移方程:dp[i - 1][j] = dp[i - 1][j] or dp[i - 1][j - nums[i]]
# 单独 1 个数可以构成子集,根据状态转移方程,当 j == nums[i] 成立的时候,会来看 dp[i - 1][0] 的值
# 因此根据语义,dp[0] 应该设置为 True
dp[0] = True
for j in range(target + 1):
if nums[0] == j:
dp[j] = True
break
for i in range(1, size):
# 先看最后一个数是不是返回 True,如果是后面就没有必要计算了,方法可以直接返回 True
dp[-1] = dp[-1] or dp[target - nums[i]]
if dp[-1]:
return True
# 然后再写倒数第 2 个数,倒数第 3 个数
for j in range(target - 1, -1, -1):
if j >= nums[i]:
dp[j] = dp[j] or dp[j - nums[i]]
else:
break
return dp[-1]