时间复杂度:O(n²)
解题思路
既然要分割等和,那么就可以看做是选一些元素凑成所有元素总和的一半,而对于每个元素都有选与不选两种状态,所以该题就可以用01背包解题。
用dp[i][j]表示前i个元素能否凑成和为j的值,这样数组中所有不小于j的元素都有两种情况,一种是被选择求和,另外一种就是不被选择被忽略掉。对于前者,dp[i][j]=dp[i-1][j-nums[i]],因为如果nums[i]被选择,那么它的状态就等同于前i-1个元素能否凑成j-nums[i]的和,如果能那么选择i也能凑出j的和,如果不能那肯定凑不出j的和;对于后者,dp[i][j]=dp[i-1][j],因为不被选择,那我们就看前i-1个元素能不能凑出总和j。最后对于这两种情况,如果有一种情况为true,那么dp[i][j]就为true。
所以状态转移方程为
dp[i][j]=dp[i-1][j]||dp[i-1][j-nums[i]],j≥nums[i]
那如果j小于nums[i]呢?肯定是不选择nums[i]呀,如果加上去不就超出我们要的和j了嘛,所以
dp[i][j]=dp[i-1][j],j<nums[i]
那么边界条件是什么呢?由于j表示目标值,所以j为0时,任何元素都可以凑出该值,也就是说,无论i为多少,只要j为0那么dp[i][j]就为true。接下来再考虑i,如果i为0表示不选择任何一个元素,所以除了0值无法凑出任何数。综上,边界条件就是
①dp[i][0]=true,i=0,1,2... ②dp[0][j]=false,j=1,2,...
仔细观察我们可以发现,i都只依赖于i-1,所以我们可以将二维数组压缩成一维数组,忽略掉i的存在只考虑j,这样我们就可以从大到小遍历更新一维数组,注意必须是从大到小,因为这样不会覆盖掉旧值。这种方法就是滚动数组。
此外我们还需要关注一下特殊情况,一是数组中的最大元素超过了总和的一半,那么无论如何分割数组都会失败,所以该情况必须返回false;二是数组和为奇数,由于元素都是整数,如果和为奇数除以2后为小数也无法分割,故也返回false;三是数组只有一个元素,该情况可以归到第一种情况中。
AC代码
func canPartition(nums []int) bool {
sum,max:=0,0
for _,num:=range nums{
sum+=num
if num>max{
max=num
}
}
//数组元素和为奇数无法分割
if sum&1==1{
return false
}
target:=sum>>1
//最大元素超过和的一半值,无法分割
if max>target{
return false
}
dp:=make([]bool,target+1)//dp[i]表示可以用数组中的元素凑出i
dp[0]=true//边界
for _,num:=range nums{
//01背包一定要从大到小遍历
for j:=target;j>=num;j--{//注意j一定要不小于元素值
dp[j]=dp[j]||dp[j-num]
}
}
return dp[target]
}
感悟
一开始想复杂了,看了题解才反应过来就是很普通的01背包变种,还是刷题数少了。
二刷感悟
很顺利地想到了01背包,但还是想不到dp[i][j]的含义和状态转移方程,还是得多背多总结。之后对一刷代码进行了优化,把只有1个元素的情况归到了最大元素值超过数组和一半的情况,还把奇偶判断和除以2改成了位运算,提升了时间效率。
扩展:01背包问题
问题描述
有n件物品,每件物品的重量为w[i],价值为c[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。其中每种物品都只有1件。
解题思路
令dp[i][v]表示前i件物品(1≤i≤n,0≤v≤V)恰好装入容量为v的背包中所能获得的最大价值。
对第i件物品的选择有两种策略:
- 不放第i件物品,那么问题转化为前i-1件物品恰好装入容量为v的背包中所能获得的最大价值,也即dp[i-1][v]。
- 放第i件物品,那么问题转化为前i-1件物品恰好装入容量为v-w[i]的背包中所能获得的最大价值,也即dp[i-1][v-w[i]]+c[i]。
因此状态转移方程为:
dp[i][v]=max{dp[i-1][v],dp[i-1][v-w[i]]+c[i]}(1≤i≤n,w[i]≤v≤V)
而由于dp[i][v]表示的是恰好为v的情况,所以需要枚举dp[n][v](0≤v≤V),取其最大值才是最后的结果。
由于状态转移方程中计算dp[i][v]时总是只需要dp[i-1][v]左侧部分的数据,且当计算dp[i+1][]的部分时,dp[i-1]的数据又完全用不到了(只需要用到dp[i][]),因此不妨可以直接开一个一维数组dp[v](即把第一维省去),枚举方向改变为i从1到n,v从V到0(逆序!),这样状态转移方程变为
dp[v]=max(dp[v],dp[v-w[i]]+c[i])(1≤i≤n,w[i]≤v≤V)
这种技巧成为滚动数组。