Leetcode 416. 分割等和子集【0-1背包】
中等题, 背包问题
题目
给你一个 只包含正整数 的 非空 数组 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
解法
这种选择类问题一般都是考虑使用背包问题求解。由于这里的“物品是只能一次使用”,因此为0-1背包问题。
给一个可装载重量为 sum / 2 的背包和 N 个物品,每个物品的重量为 nums[i]。现在让你装物品,是否存在一种装法,能够恰好将背包装满?
第一步要明确两点,「状态」和「选择」,状态就是「背包的容量」和「可选择的物品」,选择就是「装进背包」或者「不装进背包」。
-
dp 数组的定义:
dp[i][j]
= x 表示,对于前 i 个物品,当前背包的容量为 j 时,若 x 为 true,则说明可以恰好将背包装满,若 x 为 false,则说明不能恰好将背包装满。 -
根据 dp 数组含义,可以根据「选择」对
dp[i][j]
得到以下状态转移:-
如果不把 nums[i] 算入子集,或者说你不把这第 i 个物品装入背包,那么是否能够恰好装满背包,取决于上一个状态
dp[i-1][j]
,继承之前的结果。 -
如果把 nums[i] 算入子集,或者说你把这第 i 个物品装入了背包,那么是否能够恰好装满背包,取决于状态
dp[i-1][j-nums[i-1]]
。
-
class Solution:
def canPartition(self, nums: List[int]) -> bool:
psum = sum(nums)
if psum%2:return False
#01 背包
m, n = len(nums), psum//2
dp = [[False for _ in range(n+1)]for _ in range(m+1)]
for i in range(m+1): dp[i][0] = True
for i in range(1,m+1):
for j in range(1,n+1):
if j-nums[i-1]<0: dp[i][j] = dp[i-1][j] #无法选择
else: dp[i][j] = dp[i-1][j] or dp[i-1][j-nums[i-1]] #不选 or 选择
return dp[m][n]
- 时间复杂度 O ( n 2 ) O(n^2) O(n2) 求和需要O(n),动态规划需要 O ( n 2 ) O(n^2) O(n2)
- 空间复杂度 O ( n 2 ) O(n^2) O(n2)
空间压缩
二维数组遍历时只和上一行有关,因此可以压缩空间为一维数组,但是要注意在 j 遍历时需要逆序,因为更新依赖于上一行前面的信息(逆序以免覆盖)。
class Solution:
def canPartition(self, nums: List[int]) -> bool:
psum = sum(nums)
if psum%2:return False
#01 背包
n = psum//2
dp = [False for _ in range(n+1)]
dp[0] = True
for i in range(len(nums)):
for j in range(n,-1,-1):
if j-nums[i]>=0: dp[j] = dp[j] or dp[j-nums[i]]
return dp[n]
- 时间复杂度 O ( n 2 ) O(n^2) O(n2) 求和需要O(n),动态规划需要 O ( n 2 ) O(n^2) O(n2)
- 空间复杂度 O(n)