给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入: nums = 1,5,11,5
输出: true
**解释:**数组可以分割成 1, 5, 5 和 11 。
示例 2:
输入: nums = 1,2,3,5
输出: false
**解释:**数组不能分割成两个元素和相等的子集。
提示:
1 <= nums.length <= 2001 <= nums[i] <= 100
前置知识:01背包
情景:有n件物品和一个容量为bagweight的背包,第i件物品的重量为weight[i],其价值为value[i],每件物品只能用一次。求装入背包的物品的最大价值总和
暴力解法:每个物品要么被选,要么不被选。使用回溯即可。时间复杂度为O(2^n)
使用动态规划进行优化。先明确dp数组的维度及其含义。由于要考虑物品及其重量,因此先使用二维数组。dp[i][j]中,i表示物品编号,j表示背包容量,dp[i][j]表示使用容量为j的背包,从下标为0-i的物品里任意取,所能得到的最大价值总和
接着,确定递推公式
以推导dp[2][3]为例:
dp[2][3]的意思是:使用容量为3的背包,从下标为0、1、2的物品里任意取,所能得到的最大价值总和。那么要求解它,就有两种情况:放入物品2、不放入物品2
如果不放入物品2,那么这个物品2有没有都不会产生影响,因此dp[2][3]=dp[1][3]
如果放入物品2,那么就必须要给物品2留出足够的空间
如果物品2的重量已经超过了3,那么这个背包是不能放入物品2的。如果没有超过3,假设它的重量是2,那么对于容量为3的背包来说,放入物品2后还剩的容量为1,所以只需要看容量为1的背包从下标为0、1的物品里面取,所能得到的最大价值总和是多少就可以了。此时dp[2][3]=dp[1][1]+value[2]
抽象来看,求解dp[i][j]的过程为:
不放入物品i:物品i没发挥作用,dp[i][j]=dp[i-1][j]
放入物品i:要给它留位置,放入物品i前,背包容量为j-weight[i],因此dp[i][j]=dp[i-1][j-weight[i]]+value[i]
两者取最大值即可
然后看如何初始化dp数组(因为i是从i-1推导出来,因此必须要初始化):
dp[i][0]:背包容量为0,因此不能放入任何物品,所以其值初始化为0
dp[0][j]:只能选择物品0,那么就需要判断j是否不小于weight[0],如果小于就不选择,因为容量不够,其值初始化为0,反之初始化为value[0]
由递推公式可见,其它下标都可以从左上方数值推导出来,因此其它位置的元素默认初始化为0即可
所以,初始化工作实际上只需要对第0行进行处理即可,其它都初始化为0
再看dp数组的遍历顺序:
是先遍历物品还是先遍历背包?其实都可以,因为要么从正上方推导出结果,要么从左上方推导出结果
先遍历物品再遍历背包相当于先定下来列,再一行一行去填
反之,先遍历背包再遍历物品相当于先定下来行,再一列一列去填
滚动数组
从二维数组的递推公式可以看到,dp[i][j]只和dp[i-1][j]以及dp[i-1][j-weight[i]]有关系,而上一行(即i-1)又可以通过滚动数组拷贝,因此考虑优化为一维数组
优化后的递推公式为:dp[j]=max(dp[j-weight[i]]+value[i],dp[j])
理解:括号内的dp[j]是上一行的dp[j],相当于是dp[i-1][j]
接着看如何初始化这个一维数组
dp[j]表示:容量为j的背包所能容纳物品的最大价值,那么dp[0]=0,而其它位置都会由前面位置的值推导出来,因此只需要将每个位置都初始化为0即可
再看遍历顺序:正确做法是先遍历物品再倒序遍历背包容量
首先,为什么要倒序?
如果是正序遍历,同一个物品可能会被放入多次。因为我们使用的是滚动数组,每一次更新dp数组,都是看是否要将物品i加入背包中,所以如果前面有一个容量的背包已经加入了物品i,那么后面的位置再判断是否要将物品i加入背包就已经没有意义了。而倒序遍历就避免了这个问题,因为dp[j]是依赖前面的元素求出来的
举个例子:weight[0]=1,value[0]=20
如果正序遍历,那么在第一轮遍历中,dp[1]=dp[1-weight[0]]+value[0]=20
dp[2]=dp[2-weight[0]]+value[0]=40
但dp[2]的含义是:选择物品0,在容量为2的背包中所能容纳的最大价值。很显然只能选择物品0,最大价值应该是20,但这里因为算多了一次,所以变成了40
通过倒序遍历,我们才真正找到了dp[i-1][j-weight[i]],因为要看上一行的元素,因此就不能先覆盖掉它
其次,为什么先物品后背包容量?
如果先遍历背包容量再遍历物品,那么代码应该是这样的:
cpp
for(int j=bagweight;j>=1;j--) {
for(int i=0;i<weight.size();i++) {
if(j>=weight[i]) dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
}
}
假设bagweight=5,有三个物品,重量分别为1、2、3,价值分别为1、2、3:
一开始,j=5,进入内层循环:
i=0,j>=weight[0]=1,dp[5]=dp[5-1]+value[0]=0+1=1
i=1,j>=weight[1]=2,dp[5]=max(dp[5],dp[5-2]+2)=2
走到这里我们就发现不对了,明明容量为5的背包是可以同时放入重量分别为1和2的物品的,总价值应该为3,但是i=1时却为2
如果是先遍历物品,再遍历背包容量:
cpp
for(int i=0;i<weight.size();i++) {
for(int j=bagweight;j>=weight[i];j--) {
dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
}
}
还是假设bagweight=5,有三个物品,重量分别为1、2、3,价值分别为1、2、3:
第一轮,i=0,进入内层循环
j=5,dp[5]=dp[4]+1=1
j=4,dp[4]=dp[3]+1=1
以此类推,这一轮除了dp[0]=0之外,其它都为1,因为只能选择物品0,选了肯定比不选的价值高
第二轮,i=1,进入内层循环
j=5,dp[5]=max(1,dp[3]+2)=3
可以看到,这时候背包是真正放入了物品0和物品1
抽象地说,因为必须使用倒序遍历,所以如果是先遍历背包容量再遍历物品,那么由于前面的背包都还没被处理过,所以相当于你每次只能放入一个物品
因此,必须要先遍历物品,再遍历背包容量
回归题目
问题等价于:能否找到一个子序列,使得其和为数组元素和的一半?
也就是说,我们可以先求出数组元素和sum,再判断是否存在子序列使得其和为sum/2
相当于,我们现在有一个容量为sum/2的背包,我们只需要关注:是否存在这么一堆商品能够将这个背包装满?
这里数字就是物品,物品的价值和重量都是数字的值
那么我们还是要求背包所能容纳的最大价值,然后看这个最大价值是否等于背包容量即可
根据前面01背包的知识,我们知道:
dp[j]表示容量为j的背包所能容纳的最大价值dp[0] = 0- 外层遍历物品,内层遍历背包容量
- 遍历背包容量时,必须倒序遍历
Java
java
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
for(int num : nums) {
sum += num;
}
if(sum % 2 == 1) {
return false;
}
int target = sum / 2;
int[] dp = new int[10005]; //每个元素最大100,最多200个,sum/2最大为10000,因此数组大小设置为10005
//外层遍历物品,内层倒序遍历背包容量
for(int i = 0; i < nums.length; i++) {
for(int j = target; j >= nums[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
return dp[target] == target;
}
}
Python
python
class Solution:
def canPartition(self, nums: List[int]) -> bool:
n = len(nums)
if n < 2 :
return False
total = sum(nums)
if total % 2 == 1 :
return False
target = total // 2
dp = [0 for _ in range(10005)]
for i, num in enumerate(nums) :
for j in range(target, num - 1, -1) :
dp[j] = max(dp[j], dp[j - num] + num)
return dp[target] == target