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]
}
相关推荐
Huazie2 分钟前
在WSL2 Ubuntu中部署FastDFS服务的完整指南
服务器·后端·ubuntu
Java知识库26 分钟前
Java BIO、NIO、AIO、Netty面试题(已整理全套PDF版本)
java·开发语言·jvm·面试·程序员
行者无疆xcc1 小时前
【Django】设置让局域网内的人访问
后端·python·django
JiangJiang1 小时前
🚀 Vue 人如何玩转 React 自定义 Hook?从 Mixins 到 Hook 的华丽转身
前端·react.js·面试
lianghj1 小时前
前端高手必备:深度解析高频场景解决方案与性能优化实战
前端·javascript·面试
嘵奇1 小时前
基于Spring Boot实现文件秒传的完整方案
java·spring boot·后端
Value_Think_Power1 小时前
azure 一个 pod 内有多个 container ,这些container 可以 共享一块磁盘吗
后端
李菠菜1 小时前
优化Centos关闭SELinux/Swap及资源限制调整
linux·后端·centos
wangyongquan1 小时前
koa语法 | koa/router | 中间件 | 洋葱模型
后端·node.js
小兵张健1 小时前
小米 JD 调研
java·后端·面试