数据结构【动态规划-0-1背包】| leetcode 494. 目标和(中等)

代码链接:https://leetcode.cn/problems/target-sum/solution/by-flix-rkb5/

题目分析:

记数组的元素和为 total,添加 + + + 号的元素之和为 pos,添加 - 号的元素之和为 neg,则有以下关系:
{  pos  + n e g =  total   pos  − n e g =  target  \left\{\begin{array}{l} \text { pos }+n e g=\text { total } \\ \text { pos }-n e g=\text { target } \end{array}\right. { pos +neg= total  pos neg= target 
进一步可得:
{  pos  = (  total  +  target  ) / 2  neg  = (  total  −  target  ) / 2 \left\{\begin{array}{l} \text { pos }=(\text { total }+\text { target }) / 2 \\ \text { neg }=(\text { total }-\text { target }) / 2 \end{array}\right. { pos =( total + target )/2 neg =( total  target )/2

问题转化:

此时不难发现,本题实质上是一道「0-1 背包问题」:给定一个只包含正整数的非空数组 nums,判断是否可以从数组中选出一些数字(每个元素最多选一次),使得选出的这些数字的和刚好等于 pos 或者 neg。

程序执行前可先判断 n u m s nums nums 是否满足一些基本条件,如 t o t a l > t a r g e t total>target total>target t o t a l + t a r g e t total+target total+target能被 2 整除等,若不满足程序则可直接返回 0。

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 中选出若干个数字(每个元素最多选一次)使得其和刚好等于 p o s pos pos 或者 n e g neg neg,并计算有多少种不同的选择方式。

I. 状态定义

对于本题,定义 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示:从前 i i i 个数字中选出若干个,使得被选出的数字其和为 j j j 的方案数目。

II. 状态转移

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

1 ≤ i ≤ n 1 \leq i \leq n 1in 时,对于数组 nums 中的第 i i i 个元素 num ( i i i 的计数从 1 开始),遍历 0 ≤ j ≤ n e g 0 \leq j \leq n e g 0jneg ,计算 d p [ i ] [ j ] d p[i][j] dp[i][j] 的值:

  • 如果 j < n u m j<n u m j<num ,则不能选 num,此时有 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] d p[i][j]=d p[i-1][j] dp[i][j]=dp[i1][j]
  • 如果 j ≥ n u m j \geq n u m jnum ,则如果不选 num,方案数是 d p [ i − 1 ] [ j ] d p[i-1][j] dp[i1][j] ,如果选 n u m n u m num ,方案数是 d p [ i − 1 ] [ j − d p[i-1][j- dp[i1][j n u m ] n u m] num] ,此时有 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + d p [ i − 1 ] [ j − n u m ] d p[i][j]=d p[i-1][j]+d p[i-1][j-n u m] dp[i][j]=dp[i1][j]+dp[i1][jnum]

因此状态转移方程如下:
d p [ i ] [ j ] = { d p [ i − 1 ] [ j ] , j < n u m s [ i ] d p [ i − 1 ] [ j ] + d p [ i − 1 ] [ j − n u m s [ i ] ] , j ≥ n u m s [ i ] d p[i][j]= \begin{cases}d p[i-1][j], & j<n u m s[i] \\ d p[i-1][j]+d p[i-1][j-n u m s[i]], & j \geq n u m s[i]\end{cases} dp[i][j]={dp[i1][j],dp[i1][j]+dp[i1][jnums[i]],j<nums[i]jnums[i]

最终得到 d p [ n ] [ n e g ] d p[n][neg] dp[n][neg] 的值即为答案。

III. 初始化

记数组 nums 的长度为 n n n。为便于状态更新,减少对边界的判断,初始二维 d p d p dp 数组维度为 ( n + 1 ) × ( ∗ ) (n+1) \times(*) (n+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 ] nums[0] nums[0],第 0 个数字为空。

初始化时:

  • d p [ 0 ] [ 0 ] = 1 dp[0][0]=1 dp[0][0]=1:表示从前 0 个数字中选出若干个数字使得其和为 0 的方案数为 1 ,即「空集合」不选任何数字即可得到 0。
  • 对于其他 d p [ 0 ] [ j ] , j ≥ 1 dp[0][j], j \geq 1 dp[0][j],j1 ,则有 d p [ 0 ] [ j ] = 0 d p[0][j]=0 dp[0][j]=0:「空集合」无法选出任何数字使得其和为 j ≥ 1 j \geq 1 j1 d p [ i ] [ 0 ] = 1 d p[i][0]=1 dp[i][0]=1 在程序迭代实现中已有体现,在此无需提前重复定义。

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

class Solution:
    def findTargetSumWays(self, nums: List[int], target: int) -> int:
        total = sum(nums)
        if abs(target) > total:         # target可能为负
            return 0
        if (total + target) % 2 == 1:   # 不能被2整除【对应于pos不是整数】
            return 0
        
        pos = (total + target) // 2
        neg = (total - target) // 2
        capcity = min(pos, neg)         # 取pos和neg中的较小者,以使得dp空间最小
        n = len(nums)

        # 初始化
        dp = [[0] * (capcity+1) for _ in range(n+1)]
        # dp[i][j]: 从前i个元素中选出若干个其和为j的方案数
        dp[0][0] = 1        # 其他 dp[0][j]均为0
        
        # 状态更新
        for i in range(1, n+1):
            for j in range(capcity+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] = dp[i-1][j] + dp[i-1][j-nums[i-1]]
        return dp[n][capcity]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值