
解法一:子集背包问题抽象,二维dp
回忆一下 0-1 背包问题定义
给你一个可装载重量为 W
的背包和 N
个物品,每个物品有重量和价值两个属性。其中第 i
个物品的重量为 wt[i]
,价值为 val[i]
,现在让你用这个背包装物品,最多能装的价值是多少?
其中,物品不可以分割,要么装进包里,要么不装,不能说切成两块装一半。这就是 0-1 背包这个名词的来历。
解决这个问题没有什么排序之类巧妙的方法,只能穷举所有可能,采用动态规划。
为什么说这道题是背包问题?
我们可以先对集合求和,得出 sum
,然后把问题转化为背包问题:给一个可装载重量为 sum / 2
的背包和 N
个物品,每个物品的重量为 nums[i]
。现在让你装物品,是否存在一种装法,能够恰好将背包装满?
因为我们的答案只是原数组的一个子集,因此这类问题可以称为子集背包问题。
自底向上的递推dp
- 明确【状态】和【选择】:在典型背包问题中,【状态】就是背包的容量和可选择的物品列表,【选择】就是装或不装进背包,那么套入这道题中,【状态】就是「子集元素和」和「可选择的元素」,【选择】就是第i个元素加不加入子集中
- 明确dp数组定义 :dp[i][j] = x,对于前
i
个物品(i
从 1 开始计数),当前背包的容量为j
时,若x
为true
,则说明可以恰好将背包装满,若x
为false
,则说明不能恰好将背包装满 。例如,dp[4][9] = true的含义是对于给定的集合中,若只在前 4 个数字中进行选择,存在一个子集可以恰好凑出元素和为 9。根据这个定义,我们想求的最终答案就是dp[len(nums)][sum/2]
,base case 就是dp[...][0] = true
和dp[0][...] = false
,因为背包没有空间的时候,就相当于装满了,而当没有物品可选择的时候,肯定没办法装满背包。 - 明确状态转移 :如果不把
nums[i]
加入子集,或者说你不把这第i
个物品装入背包,那么是否能够恰好装满背包,取决于上一个状态dp[i-1][j]
,继承之前的结果;如果把nums[i]
加入子集,或者说你把这第i
个物品装入了背包,那么是否能够恰好装满背包,取决于状态dp[i-1][j-nums[i-1]]
go
func canPartition(nums []int) bool {
var sum int
for _, v := range nums{
sum += v
}
if sum %2 != 0{ // 奇数和无法分割为两个元素和(sum/2)相同的子集
return false
}
// dp[i][j]表示在数组的前i个元素中,能否凑出元素和为j的子集
// 那么,本题就是要找到dp[n][sum/2],n为nums的长度
dp := make([][]bool, len(nums)+1) // 考虑0个元素的情况,长度+1
for i := range dp{
dp[i] = make([]bool, sum/2 + 1) // 考虑和为0的情况,长度+1
// base case
dp[i][0] = true
}
// 递推更新dp
// dp[0][j](j>0)肯定为false,i从 1开始遍历即可
for i:= 1; i<len(dp); i++{
for j:=1; j<len(dp[0]); j++{
// 由于 dp数组定义中的 i是从 1开始计数,而数组索引是从 0开始的,所以第 i个元素值应该是nums[i-1]
if nums[i-1] > j{ // 第i个元素超出元素和,不能加入
dp[i][j] = dp[i-1][j]
}else{ // 选择加入或不加入,有一种满足答案即可
dp[i][j] = dp[i-1][j] || dp[i-1][j - nums[i-1]]
}
}
}
return dp[len(nums)][sum/2]
}
优化空间
注意到 dp[i][j]
都是通过上一行 dp[i-1][..]
转移过来的 ,之前的数据都不会再使用了。可以将二维 dp
数组压缩为一维,节约空间复杂度。
go
func canPartition(nums []int) bool {
var sum int
for _, v := range nums{
sum += v
}
if sum %2 != 0{ // 奇数和无法分割为两个元素和(sum/2)相同的子集
return false
}
dp := make([]bool, sum/2+1)
// base case
dp[0] = true
for i := 0; i < len(nums); i++ {
for j := sum/2; j >= 0; j-- {
if j - nums[i] >= 0 {
dp[j] = dp[j] || dp[j-nums[i]]
}
}
}
return dp[sum/2]
}