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数组定义 :dpij = x,对于前 i 个物品(i 从 1 开始计数),当前背包的容量为 j 时,若 xtrue,则说明可以恰好将背包装满,若 xfalse,则说明不能恰好将背包装满 。例如,dp49 = 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]
}
相关推荐
码事漫谈26 分钟前
1999年的电商前夜和2026年的AI前夜 历史押韵但从不重复
后端
Conan在掘金1 小时前
鸿蒙报错速查:arkts-strict-typing Property does not exist on type 'object',object 装函数就炸,根因 + 真解法
后端
颜酱1 小时前
01 | 骨架搭建:FastAPI + Vue 跑通第一个 SSE 流式问答
前端·人工智能·后端
程序员cxuan2 小时前
Grok Build 被众人唾骂,结果老马把它开源了
人工智能·后端·程序员
从零开始的代码生活_2 小时前
C++ 继承详解:访问控制、对象模型、菱形继承与设计取舍
开发语言·c++·后端·学习·算法
Conan在掘金2 小时前
�鸿蒙报错速查:Cannot find name 'require',禁用 require(),根因 + 真解法
后端
隔窗听雨眠2 小时前
Spring Boot在云原生时代的编程范式革新研究
spring boot·后端·云原生
一朵小花2 小时前
记一次Docker 容器 NFS 文件写入延迟排查过程
后端
程序员Rock3 小时前
上位机开发-MODBUS面试常见问题
c语言·c++·面试·职场和发展·上位机
FF2501_940228584 小时前
Grid 构建月历网格:7 列模板 + 日期占位算法
后端·华为·harmonyos·鸿蒙系统