动态规划0-1背包问题滚动数组

1、经典0-1背包问题

问题描述:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内选择物品使得物品的总价值最高。
回顾对于二维的0-1背包问题递推关系式:
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] ) dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]) dp[i][j]=max(dp[i1][j],dp[i1][jweight[i]]+value[i])
d p [ i ] [ j ] dp[i][j] dp[i][j]表示的是从0到i标号的物品中放入承重为j的背包中最大的价值总和。
首先,对于此公式的解释:每次选择只有两种情况,在物品 0 , 1 , . . . , i {0,1,...,i} 0,1,...,i中不选择和选择物品 i i i。对于边界初始化: d p [ 0 ] [ 0 ] = d p [ i ] [ 0 ] = 0 , d p [ 0 ] [ j ] = v a l u e [ j ] dp[0][0]=dp[i][0]=0, dp[0][j]=value[j] dp[0][0]=dp[i][0]=0,dp[0][j]=value[j]其他数组中的值全部置为0,这是因为每次更新都取最大值,且所以这些值都会被覆盖,所以初始化全0是没问题的。同时,遍历次序物品先还是背包先,顺序还是逆序都是可以得到正确结果的。

2、滚动数组

回顾我们在做斐波那契数列的时候,空间复杂度可以从O(N)降到O(1)(用两个位置去储存即可),这也是滚动数组的基本思想,做空间压缩。
从上面的递推关系我们可以发现, d p [ i ] [ j ] dp[i][j] dp[i][j]只和第一维为 i − 1 i-1 i1的dp数组有关,所以可以将递推公式中的 i − 1 i-1 i1替换为 i i i,将第 i − 1 i-1 i1层的结果写入 i i i层中,即 d p [ i ] [ j ] = m a x ( d p [ i ] [ j ] , d p [ i ] [ j − w e i g h t [ i ] + v a l u e [ i ] ) dp[i][j] = max(dp[i][j], dp[i][j-weight[i] + value[i]) dp[i][j]=max(dp[i][j],dp[i][jweight[i]+value[i])这说明第一个维度只用存储一个数据,这就是可以二维压缩到一维的原因。 d p [ j ] dp[j] dp[j]表示的是容量大小为 j j j的背包能达到的最大价值和。对比二维的递推表达,我们有如下一维递推: d p [ j ] = m a x ( d p [ j ] , d p [ j − w e i g h t [ i ] ] + v a l u e [ i ] ) dp[j] = max(dp[j], dp[j-weight[i]] + value[i]) dp[j]=max(dp[j],dp[jweight[i]]+value[i])不放物品 i i i d p [ j ] dp[j] dp[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的结果和二维的类似可得。初始化,根据一维 d p dp dp数组的含义,显然 d p [ 0 ] = 0 dp[0]=0 dp[0]=0,同样这里的 d p [ i ] dp[i] dp[i]可以置为0。关于遍历次序的理解,这里以一个例子来说明:假设有标号0,1,2的三个物品weight分别为1,3,4;value分别为15,20,30。遍历的顺序这里采用的是先遍历物品再逆序遍历背包。首先说明这样遍历的可行性。

 for i in range(3):
     for j in range(4, weight[i] - 1, -1):
         dp[j] = max(dp[j], dp[j - weight[i]] + value[i])

注意到 i = 0 , j = 4 , d p [ 4 ] = v a l u e [ 0 ] = 15 = d p [ 0 ] [ 4 ] i = 0 , j = 3 , d p [ 3 ] = v a l u e [ 0 ] = 15 = d p [ 0 ] [ 3 ] . . . i = 1 , j = 4 , d p [ 4 ] = m a x ( v a l u e [ 1 ] + d p [ 4 − w e i g h t [ 1 ] ] , d p [ 4 ] ) = 35 = d p [ 1 ] [ 4 ] . . i=0,j=4,dp[4]=value[0]=15=dp[0][4]\\ i=0,j=3,dp[3]=value[0]=15=dp[0][3]\\...\\ i=1,j=4,dp[4]=max(value[1]+dp[4-weight[1]],dp[4])=35=dp[1][4]\\.. i=0,j=4,dp[4]=value[0]=15=dp[0][4]i=0,j=3,dp[3]=value[0]=15=dp[0][3]...i=1,j=4,dp[4]=max(value[1]+dp[4weight[1]],dp[4])=35=dp[1][4]..
最后得到的每个 d p [ j ] = d p [ 2 ] [ j ] dp[j]=dp[2][j] dp[j]=dp[2][j]。而如果将背包承重遍历改变为顺序:

i = 0 , j = 0 , d p [ 0 ] = 0 = d p [ 0 ] [ 0 ] i = 0 , j = 1 , d p [ 1 ] = v a l u e [ 0 ] = 15 = d p [ 0 ] [ 1 ] i = 0 , j = 2 , d p [ 2 ] = m a x ( d p [ 1 ] + v a l u e [ 0 ] , d p [ 2 ] ) = 15 + 15 = 30 ≠ d p [ 0 ] [ 2 ] = 15 . . . i=0,j=0,dp[0]=0=dp[0][0]\\ i=0,j=1,dp[1]=value[0]=15=dp[0][1]\\ i=0,j=2,dp[2]=max(dp[1]+value[0],dp[2])=15+15=30\neq dp[0][2]=15\\... i=0,j=0,dp[0]=0=dp[0][0]i=0,j=1,dp[1]=value[0]=15=dp[0][1]i=0,j=2,dp[2]=max(dp[1]+value[0],dp[2])=15+15=30=dp[0][2]=15...
可以看到 d p [ 2 ] dp[2] dp[2]的结果显然是不对的,物品0被放入了两次。这是因为计算dp[2]的时候又用到了更新之后的dp[1]。可以对照二维dp的情况,i=0的情况都进行了初始化且没有相互影响。说的更明白一点,比如 i = 1 , d p [ 3 ] i=1,dp[3] i=1,dp[3]的结果会用到 d p [ 2 ] = d p [ 0 ] [ 2 ] dp[2]=dp[0][2] dp[2]=dp[0][2],而 d p [ 2 ] dp[2] dp[2]在此之前已经更新为 d p [ 1 ] [ 2 ] dp[1][2] dp[1][2],这样计算的 d p [ 3 ] dp[3] dp[3]肯定不对了。同理,交换物品和背包承重的循环顺序仍然会计算错误。这一块最好还是实际画一遍dp数组遍历过程才会更清楚。
下面以一个实际的例子来看。

3、实例

力扣分隔等和子集: 给你一个只包含正整数的非空数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
https://leetcode.cn/problems/partition-equal-subset-sum/

1、问题分析:从数组中选出若干数字,使得数字总价值为sum/2。对比01背包问题,nums的元素相当于物品重量,sum相等于背包承重。
2、设置dp数组和下标含义: d [ i ] [ j ] d[i][j] d[i][j]表示否存在从下标 [ 0 , i ] [0,i] [0,i]中选取元素,使得和为恰好为 j j j的情况,存在则返回True。
3、递推关系式:
A、不选下标为i的元素: d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j]=dp[i-1][j] dp[i][j]=dp[i1][j]
B、选择下标为i的元素,因为数组中元素都是正整数,其中还要分两类来看:1)如果 n u m s [ i ] = = j nums[i]==j nums[i]==j d p [ i ] [ j ] = T r u e dp[i][j]=True dp[i][j]=True
2)如果 n u m s [ i ] < j nums[i]<j nums[i]<j d p [ i ] [ j ] = d p [ i − 1 ] [ j − n u m s [ i ] ] dp[i][j]=dp[i-1][j-nums[i]] dp[i][j]=dp[i1][jnums[i]]
3)如果 n u m s [ i ] > j nums[i]>j nums[i]>j,归为第一类情况。
4、初始化: d p [ 0 ] [ 0 ] = F a l s e dp[0][0]=False dp[0][0]=False

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        # 判断特殊条件
        sum1 = sum(nums)
        if sum1%2 != 0 or len(nums) < 2 or sum1 < 2 * max(nums):
            return False
        sum1 = sum1 // 2
        n = len(nums)
        # 初始化dp
        dp = [[False for x in range(sum1+1)] for x in range(n)]
        dp[0][nums[0]] = True
        # 递推:
        for i in range(1,n):
            for j in range(1,sum1+1):
                if nums[i] == j:
                    dp[i][j] = True
                elif 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[n-1][sum1]

注意到这个问题中, d p [ i ] dp[i] dp[i]层的结果只和 d p [ i − 1 ] dp[i-1] dp[i1]的相关,所以依然可以压缩为一维dp数组。

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        # 判断特殊条件
        sum1 = sum(nums)
        if sum1%2 != 0 or len(nums) < 2 or sum1 < 2 * max(nums):
            return False
        sum1 = sum1 // 2
        n = len(nums)
        dp = [False for x in range(sum1+1)]
        for i in range(1,n):
            for j in range(sum1,0,-1):
                if nums[i] == j:
                    dp[j] = True
                elif nums[i] < j:
                    dp[j] = dp[j-nums[i]] or dp[j]
                else:
                    dp[j] = dp[j]   
        return dp[sum1]
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值