数据结构与算法——经典背包问题

一、0-1背包问题

https://www.acwing.com/problem/content/description/2/
思路:设状态dp[i][j]的含义是装入前i件物品装入容量为j的背包里时的最大总价值,那么状态转移方程为:
在这里插入图片描述
因为dp[i][j]只和dp[i-1][j]有关,所以状态可以转为一维的。在遍历j的时候,要逆序遍历,因为要保证较小的j是上一轮的状态:
dp[j]=max⁡(dp[j],dp[j-v[i]+w[i]])
比如i = 3, j = 8, v[3] = 5, w[3] = 1,一维的转移方程为dp[8] = max(dp[8],dp[3] + w[3]) 要保证dp[8]和dp[3]都是上一轮的状态。

class Solution:
    def max_value(self, N, V_max, v, w):
        # N: 物品总数
        # V_max: 背包的容量
        # v: 物品体积列表
        # w: 物品价值列表
        dp = [[0 for _ in range(V_max + 1)] for _ in range(N + 1)]
        
        for i in range(1, N + 1):
            for j in range(1, V_max + 1):
                if j >= v[i]:
                    dp[i][j] = max(dp[i - 1][j - v[i]] + w[i], dp[i - 1][j])
                else:
                    dp[i][j] = dp[i - 1][j]
        return dp[-1][-1]
class Solution:
    def max_value(self, N, V_max, v, w):
        dp  = [0] * (V_max + 1)
        for i in range(1, N + 1):
            for j in range(V_max, v[i] - 1, -1):
                dp[j] = max(dp[j], dp[j - v[i]] + w[i])
        return dp[-1]

二、完全背包问题

https://www.acwing.com/problem/content/description/3/
思路:与0-1背包问题不同的是,每件物品可以用无限次。0-1背包中,第i件物品拿还是不拿依赖于上一轮i-1的状态,这个状态绝对不会出现拿了第i个物品的情况,在j的循环中是逆序的,这保证了每件物品只拿一次。而完全背包问题中,在拿第i件物品时,可能正需要考虑已经拿了第i件物品且容量为j-v[i]的情况,即dp[i][j-v[i]],所以j要顺序遍历

class Solution:
    def max_value(self, N, V_max, v, w):
        dp  = [0] * (V_max + 1)
        for i in range(1, N + 1):
            for j in range(v[i], V_max + 1):
                dp[j] = max(dp[j], dp[j - v[i]] + w[i])
        return dp[-1]

三、分割等和子集

此题为leetcode第416题
思路:此题完全可以和0-1背包问题类比起来。要想使两个子集的元素和相等,那么nums的所有元素和必须为偶数。设nums的所有元素加起来除2取整为sums,这里的sums可以认为是背包的体积,nums的元素个数为物品的个数,nums[i]为第i个物品所占的体积。设状态dp[i][j]的含义为,对于nums里前i个数,背包体积为j时,背包能否放满。如果不装nums[i]这个物品,那么能否装满背包取决于上个状态nums[i-1][j];如果装nums[i]这个物品,那么能否装满这个背包取决于nums[i-1][j-nums[i]]。类比于0-1背包问题,二维状态完全可以写成一维的状态。

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        if len(nums) == 1:
            return False
        n = len(nums)
        sums = sum(nums)
        if sums % 2 != 0:
            return False
        sums //= 2
        
        dp = [[False for _ in range(sums + 1)] for _ in range(n)]
        dp[0][nums[0]] = True 
        
        for i in range(1, n):
            for j in range(sums + 1):
                if nums[i] <= j:
                    dp[i][j] = dp[i - 1][j - nums[i]] or dp[i - 1][j]
                else:
                    dp[i][j] = dp[i - 1][j]
        return dp[-1][-1]
class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        if len(nums) == 1:
            return False
        n = len(nums)
        sums = sum(nums)
        if sums % 2 != 0:
            return False
        sums //= 2

        dp = [False for _ in range(sums + 1)]
        dp[nums[0]] = True
        
        for i in range(1, n):
            for j in range(sums, nums[i] - 1, -1):
                dp[j] = dp[j] or dp[j - nums[i]]
        return dp[-1]

四、目标和

此题为leetcode第494题
思路:也是类似于背包问题,设状态dp[i][j]的含义是,对于nums的前i个数,和为j的组合个数。对于nums[i],它可以添加正号或负号,因此状态转移方程由两部分组成:

d p [ i ] [ j ] = d p [ i − 1 ] [ j + n u m s [ i ] ] + d p [ i − 1 ] [ j − n u m s [ i ] dp[i][j]=dp[i-1][j+nums[i]]+dp[i-1][j-nums[i] dp[i][j]=dp[i1][j+nums[i]]+dp[i1][jnums[i]

由于目标和s可能为负数,dp的下角标j为负数的话可能会有问题,我们这里给它添加一个偏置,先使目标和都为正数,然后将答案对应过去。设nums的总和为sums,那么nums里的元素经过加减后的范围是[-sums, +sums],总共有t = 2 * sums + 1种可能,因此状态dp是len(nums) * t维的。另外状态转移方程也要注意边界情况,j-sums[i]和j+sums[i]要在[0, t)之间。
这题当然也可以用一维状态解决,不过需要绕一下弯。设nums里面加正号的几个元素组成数组P,加符号的几个元素组成数组N,那么有sum§ – sum(N) = s,可得sum§ + sum(N) + sum§ – sum(N) = s + sum§ + sum(N),进一步得2 * sum§ = s + sum(nums),得到

s u m ( P ) = ( s + s u m ( n u m s ) ) 2 sum(P)=\frac{(s+sum(nums))}{2} sum(P)=2(s+sum(nums))

则问题转化为找到一个整数子集P,使得上式成立。根据上式,s + sum(nums)必须为偶数,否则返回0。问题转为0-1背包问题,组成sum§的方式有多少种,每个数字只取一次。设状态dp[i]的含义是,组成i的方式有多少种。状态转移方程为:

d p [ j ] = d p [ j ] + d p [ j − n u m s [ i ] ] dp[j]=dp[j]+dp[j-nums[i]] dp[j]=dp[j]+dp[jnums[i]]

class Solution:
    def findTargetSumWays(self, nums: List[int], s: int) -> int:
        n = len(nums)
        sums = sum(nums)
        if s > sums:
            return 0
        t = sums * 2 + 1

        dp = [[0 for _ in range(t)] for _ in range(n)]
        dp[0][nums[0] + sums] = 1   # nums[0]+sums的组合个数,只有1个
        dp[0][-nums[0] + sums] += 1 # 如果nums[0]为0,组合个数有2个,所以是+=
        
        for i in range(1, n):
            for j in range(t):  # s可能是负的,sums相当于是个偏置
                temp1 = dp[i - 1][j - nums[i]] if j - nums[i] >= 0 else dp[i - 1][0]
                temp2 = dp[i - 1][j + nums[i]] if j + nums[i] < t else dp[i - 1][0]
                dp[i][j] = temp1 + temp2
        return dp[-1][s+sums]
class Solution:
    def findTargetSumWays(self, nums: List[int], s: int) -> int:
        n = len(nums)
        sums = sum(nums)
        if s > sums:
            return 0
        if (sum(nums) + s) % 2 == 1:
            return 0
            
        p = (s + sums) // 2
        dp = [0] * (p + 1)
        dp[0] = 1
        for i in range(n):
            for j in range(p, nums[i] - 1, -1):
                dp[j] += dp[j - nums[i]]
        return dp[-1]
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值