数据结构【动态规划-0-1背包】| leetcode 416. 分割等和子集(中等)

代码链接:https://leetcode.cn/problems/partition-equal-subset-sum/solution/by-flix-szk7/
视频链接:https://space.bilibili.com/525438321/channel/seriesdetail?sid=672176

0-1背包:有n种物品,每种物品只有一个
完全背包:有n种物品,每种物品有无限个
多重背包:有n种物品,每种物品的个数各不相同

背包问题一般包括两层循环,一层遍历物品,一层遍历背包的容量。二维数组,两层循环顺序可以颠倒,

01背包问题

动态规划是解决「0-1 背包问题」的标准做法。一般地,我们定义: d p [ i ] [ j ] dp[i][j] dp[i][j] 表示前 i i i 件物品放入一个容量为 j j j 的背包可以获得的最大价值,则状态转移过程可表示为:

  • 不选择 i i i 件物品:问题转化为了前 i − 1 i-1 i1 件物品放入容量为 j j j 的背包中所获得的价值: d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j]=dp[i-1][j] dp[i][j]=dp[i1][j]
  • 选择 i i i 件物品:第 i i i 件物品占据容量 w i w_i wi,前 i − 1 i-1 i1 件物品放入剩下的容量为 j − w i j-w_i jwi 的背包中,问题也就转化为了前 i − 1 i-1 i1 件物品放入容量为 j − w i j-w_i jwi 的背包中所获得的价值 d p [ i − 1 ] [ j − w i ] dp[i-1][j-w_i] dp[i1][jwi] 加上要放入的第 i i i 件物品的价值 v i v_i vi dp[i][j]=dp[i-1][j-w_i]+v_i。注意,能放入第 i i i 件物品的前提为: w i ≤ j w_i \leq j wij

两种情况取较大者:
d p [ i ] [ j ] = max ⁡ { d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w i ] + v i } d p[i][j]=\max \left\{d p[i-1][j], d p[i-1]\left[j-w_i\right]+v_i\right\} dp[i][j]=max{dp[i1][j],dp[i1][jwi]+vi}

求最优解的背包问题中,有的题目要求 恰好装满背包 时的最优解,有的题目则要求 不超过背包容量 时的最优解。一种区别这两种问法的实现方法是在状态初始化的时候有所不同。[摘自@ 《背包问题九讲》 (网页版) (PDF版)]

初始化的 d p dp dp 数组事实上就是在背包中没有放入任何物品时的合法状态:

  • 如果要求恰好装满背包,那么在初始化时 d p [ i ] [ 0 ] = 0 dp[i][0]=0 dp[i][0]=0,其它 d p [ i ] [ 1 , 2 , . . . , ∗ ] dp[i][1,2,...,∗] dp[i][1,2,...,] 均设为 − ∞ -\infty 。这是因为此时只有容量为 0 的背包可能被价值为 0 的 nothing “恰好装满”,而其它容量的背包均没有合法的解,属于未定义的状态。
  • 如果只是要求不超过背包容量而使得背包中的物品价值尽量大,初始化时应将 d p [ ∗ ] [ ∗ ] dp[∗][∗] dp[][] 全部设为 0。这是因为对应于任何一个背包,都有一个合法解为 “什么都不装”,价值为 0。

本题题目分析:

对于本题而言, n u m s [ i ] nums[i] nums[i] 则对应于常规背包问题中第 i i i 件物品的重量。我们要做的是判断是否可以将数组 n u m s nums nums 分割成两个子集,使得两个子集的元素和相等,这是一个 T r u e / F a l s e True / False True/False 的存在问题。我们 的目标是从 n u m s nums nums 中选出若干个数字使其和恰好等于数组总和的一半,记为 target = s u m ( n u m s ) 2 \text{target}=\frac{{sum}(nums)}{2} target=2sum(nums)

I. 状态定义

对于本题,定义 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示从前 i i i 个数字中选出若干个,刚好可以使得被选出的数字其重量和为 j j j

II. 状态转移

根据本题的要求,上述「0-1 背包问题」的状态转移方程 (1) 可修改为:

d p [ i ] [ j ] = d p [ i − 1 ] [ j ]  or  d p [ i − 1 ] [ j − n u m s [ i − 1 ] ] dp[i][j]=d p[i-1][j] \quad \text { or } \quad d p[i-1][j-n u m s[i-1]] dp[i][j]=dp[i1][j] or dp[i1][jnums[i1]]

对于 i > 0 i>0 i>0 j > 0 j>0 j>0 的情况,如何确定 d p [ i ] [ j ] dp[i][j] dp[i][j] 的值?需要分别考虑以下两种情况。

  • 如果 j ≥ n u m s [ i ] j \geq nums[i] jnums[i],则对于当前的数字 n u m s [ i ] nums[i] nums[i] ,可以选取也可以不选取,两种情况只要有一个为 true,就有 d p [ i ] [ j ] = t r u e dp[i][j]=true dp[i][j]=true
    • 如果不选取 n u m s [ i ] nums[i] nums[i] ,则 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j]=d p[i-1][j] dp[i][j]=dp[i1][j]
    • 如果选取 nums [ i ] [i] [i] ,则 d p [ i ] [ j ] = d p [ i − 1 ] [ j − n u m s [ i ] ] d p[i][j]=dp[i-1][j-nums[i]] dp[i][j]=dp[i1][jnums[i]]
  • 如果 j < n u m s [ i ] j<nums[i] j<nums[i],则在选取的数字的和等于 j j j 的情况下无法选取当前的数字 n u m s [ i ] nums[i] nums[i],因此有 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j]=dp[i-1][j] dp[i][j]=dp[i1][j]

状态转移方程如下:

d p [ i ] [ j ] = { d p [ i − 1 ] [ j ] ∣ d p [ i − 1 ] [ j − n u m s [ i ] ] , j ≥ n u m s [ i ] d p [ i − 1 ] [ j ] , j < n u m s [ i ] d p[i][j]= \begin{cases}d p[i-1][j] \mid d p[i-1][j-n u m s[i]], & j \geq n u m s[i] \\ d p[i-1][j], & j<n u m s[i]\end{cases} dp[i][j]={dp[i1][j]dp[i1][jnums[i]],dp[i1][j],jnums[i]j<nums[i]

最终得到 d p [ n − 1 ] [ t a r g e t ] d p[n-1][target] dp[n1][target] 即为答案。

III. 初始化

记非空数组 nums 的长度为 n n n 。为便于状态更新,减少对边界的判断,初始二维 d p dp dp 数组维度为 ( n + 1 ) × ( t a r g e t + 1 ) (n+1) \times(target+1) (n+1)×(target+1) ,其中第一维为 n + 1 n+1 n+1 也意味着:第 i i i 个数字为 n u m s [ i − 1 ] nums[i-1] nums[i1],第 1 个数字为 n u m s [ 0 ] n u m s[0] nums[0] ,第 0 个数字为空。

初始化时:

  • d p [ 0 ] [ 0 ] = d p[0][0]= dp[0][0]= True:表示可以从前 0 个数字中选出若干个数字使得其和为 0 ,即「空集合」不选任何数字即可得到 0 。
  • 对于其他 d p [ 0 ] [ j ] , j ≥ 1 dp[0][j], j \geq 1 dp[0][j],j1 ,则有 d p [ 0 ] [ j ] = d p[0][j]= dp[0][j]= False:「空集合」无法选出任何数字使得其和为 j ≥ 1 j \geq 1 j1

d p [ i ] [ 0 ] = d p[i][0]= dp[i][0]= True 在程序迭代实现中已有体现,在此无需提前重复定义。

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        total = sum(nums)
        if total % 2 == 1:   # 总和无法等分
            return False
        
        target = total // 2  # 最大值大于总和的一半,无法分割
        if max(nums) > target:
            return False
        
        n = len(nums)
        # dp[i][j]: 从前i个元素中选出若干个数字刚好能够组成j
        dp = [[False] * (target+1) for _ in range(n)]
        dp[0][0] = True  # 其他 dp[0][j]均为False

        for i in range(1, n):          # 一层遍历物品
            for j in range(target+1):  # 一层遍历背包的容量
                if j < nums[i-1]:   # 容量有限,无法选择第i个数字nums[i-1]
                    dp[i][j] = dp[i-1][j]
                else:               # 可选择第i个数字nums[i-1],也可不选
                    # dp[i][j] = max(dp[i-1][j-nums[i-1]], dp[i-1][j])
                    dp[i][j] = dp[i-1][j-nums[i-1]] | dp[i-1][j]
        return dp[n-1][target]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值