LeetCode416 分割等和子集

leetcode.cn/problems/pa...

解法一:子集背包问题抽象,二维dp

回忆一下 0-1 背包问题定义

给你一个可装载重量为 W 的背包和 N 个物品,每个物品有重量和价值两个属性。其中第 i 个物品的重量为 wt[i],价值为 val[i],现在让你用这个背包装物品,最多能装的价值是多少?

其中,物品不可以分割,要么装进包里,要么不装,不能说切成两块装一半。这就是 0-1 背包这个名词的来历。

解决这个问题没有什么排序之类巧妙的方法,只能穷举所有可能,采用动态规划。

为什么说这道题是背包问题?

我们可以先对集合求和,得出 sum,然后把问题转化为背包问题:给一个可装载重量为 sum / 2 的背包和 N 个物品,每个物品的重量为 nums[i]。现在让你装物品,是否存在一种装法,能够恰好将背包装满

因为我们的答案只是原数组的一个子集,因此这类问题可以称为子集背包问题。

自底向上的递推dp
  1. 明确【状态】和【选择】:在典型背包问题中,【状态】就是背包的容量和可选择的物品列表,【选择】就是装或不装进背包,那么套入这道题中,【状态】就是「子集元素和」和「可选择的元素」,【选择】就是第i个元素加不加入子集中
  2. 明确dp数组定义 :dp[i][j] = x,对于前 i 个物品(i 从 1 开始计数),当前背包的容量为 j 时,若 xtrue,则说明可以恰好将背包装满,若 xfalse,则说明不能恰好将背包装满 。例如,dp[4][9] = true的含义是对于给定的集合中,若只在前 4 个数字中进行选择,存在一个子集可以恰好凑出元素和为 9。根据这个定义,我们想求的最终答案就是 dp[len(nums)][sum/2],base case 就是 dp[...][0] = truedp[0][...] = false,因为背包没有空间的时候,就相当于装满了,而当没有物品可选择的时候,肯定没办法装满背包。
  3. 明确状态转移 :如果不把 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]
}
相关推荐
Piper蛋窝2 小时前
深入 Go 语言垃圾回收:从原理到内建类型 Slice、Map 的陷阱以及为何需要 strings.Builder
后端·go
六毛的毛4 小时前
Springboot开发常见注解一览
java·spring boot·后端
AntBlack4 小时前
拖了五个月 ,不当韭菜体验版算是正式发布了
前端·后端·python
31535669134 小时前
一个简单的脚本,让pdf开启夜间模式
前端·后端
uzong5 小时前
curl案例讲解
后端
一只叫煤球的猫6 小时前
真实事故复盘:Redis分布式锁居然失效了?公司十年老程序员踩的坑
java·redis·后端
yanlele6 小时前
前端面试第 75 期 - 2025.07.06 更新前端面试问题总结(12道题)
前端·javascript·面试
大鸡腿同学6 小时前
身弱武修法:玄之又玄,奇妙之门
后端
轻语呢喃8 小时前
JavaScript :字符串模板——优雅编程的基石
前端·javascript·后端
MikeWe8 小时前
Paddle张量操作全解析:从基础创建到高级应用
后端