题目描述
问题转化
可以理解成是否能从给定数组里取出部分元素,使这些元素总和刚好为数组总和的一半。由于每个元素只能使用一次,所以就是0-1背包问题了。
二维数组解法
- 确定dp数组及下标含义
dp[i][j]表示是否能从下标不大于i的元素中取出部分元素使得这些元素总和刚好为j(值为True或False)。j的范围是0~target,target就是数组总和的一半。 - 递推公式
dp[i][j]=dp[i-1][j] or dp[i-1][j-nums[i]]
两种选择:
1)不使用下标为i的元素,此时dp[i][j]=dp[i-1][j]
2)使用下标为i的元素,此时dp[i][j]=dp[i-1][j-nums[i]]
这两种选择满足其一即为True,所以用or运算符连接。 - 初始化
假如数组为[1,2,3],target=3,则数组如下。
dp = [[False] * n for i in range(m)]
首先将第1列初始化为True,可以理解成当target为0时,一个元素都不取,这也算一种选择。
然后初始化第一行,递推公式为
dp[0][j] = dp[0][j - nums[0]]
这里要用倒序遍历,因为每个元素只能用一次。
- 双重遍历填表
完整代码如下:
def canPartition(self, nums: List[int]) -> bool:
total = sum(nums)
if total % 2 == 1 or len(nums) == 1:
return False
m = len(nums)
n = total // 2 + 1
dp = [[False] * n for i in range(m)]
for i in range(m):
dp[i][0] = True
for j in range(n - 1, 0, -1):
if j >= nums[0]:
dp[0][j] = dp[0][j - nums[0]]
for i in range(1, m):
for j in range(1, n):
if j < nums[i]:
dp[i][j] = dp[i - 1][j]
else:
dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i]]
return dp[i][j]
滚动数组
因为当前值只和上一层有关,所以我们可以用滚动数组代替原来的二维数组来降低空间复杂度。
所谓的滚动数组就是不断用当前层的值覆盖上一层的值,所以要注意在内层循环中一定要逆序。不然当左边的值被新的值覆盖后,再计算后面的值时就会出错。
def canPartition(self, nums: List[int]) -> bool:
total = sum(nums)
if total % 2 == 1 or len(nums) == 1:
return False
m = len(nums)
n = total // 2 + 1
dp = [False] * n
dp[0] = True
for i in range(m):
for j in range(n - 1, 0, -1):
if j < nums[i]:
dp[j] = dp[j]
else:
dp[j] = dp[j] or dp[j - nums[i]]
return dp[j]
在完全背包问题中不用逆序,因为元素可以重复取,所以当前层左边的元素可以被覆盖。