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]
}
相关推荐
小华同学ai21 分钟前
千万别错过!这个国产开源项目彻底改变了你的域名资产管理方式,收藏它相当于多一个安全专家!
前端·后端·github
独行soc24 分钟前
2025年渗透测试面试题总结-腾讯[实习]玄武实验室-安全工程师(题目+回答)
linux·安全·web安全·面试·职场和发展·渗透测试·区块链
Vowwwwwww24 分钟前
GIT历史存在大文件的解决办法
前端·git·后端
捡田螺的小男孩36 分钟前
京东一面:接口性能优化,有哪些经验和手段
java·后端·面试
艾露z44 分钟前
深度解析Mysql中MVCC的工作机制
java·数据库·后端·mysql
我是小七呦1 小时前
😧纳尼?前端也能做这么复杂的事情了?
前端·面试·ai编程
前端付豪1 小时前
揭秘网易统一日志采集与故障定位平台揭秘:如何在亿级请求中1分钟定位线上异常
前端·后端·架构
陈随易1 小时前
Lodash 杀手来了!es-toolkit v1.39.0 已完全兼容4年未更新的 Lodash
前端·后端·程序员
未来影子1 小时前
SpringAI(GA):Nacos3下的分布式MCP
后端·架构·ai编程
前端小巷子2 小时前
Promise 基础:异步编程的救星
前端·javascript·面试