【lintcode】背包问题

目录

92. 0/1背包问题(无价值)

125. 0/1背包问题 II(有价值)

总结1

0/1背包问题V-方案个数

完全背包问题IV-方案个数


92. 0/1背包问题(无价值)

在n个物品(每一个物品只能被装一次)中挑选若干物品装入背包,最多能装多满?假设背包的大小为m,每个物品的大小为A[i]
样例 1:
    输入:  [3,4,8,5], backpack size=10
    输出:  9
样例 2:
    输入:  [2,3,5,7], backpack size=12
    输出:  12

  • 二维的dp数组

dp[i][j]表示对于大小为j的背包,前i个物品能装多满。所谓0/1背包问题,就是对于某一个物品,它有两种状态,要么放入背包,状态为1,要么不放入背包,状态为0,因此可分两种情况:

(1)第i个物品没有放入背包中,那么此时背包装的大小为dp[i][j] = dp[i-1][j]

(2)第i个物品放入背包中,那么此时背包装的大小为dp[i][j] = dp[i-1][j-A[i]]+A[i]

由于我们求得是最多能装多满,因此是最大值,即dp[i][j] =max(dp[i-1][j-A[i]]+A[i],dp[i-1][j])

因此代码如下:

        for j in range(m+1):#首先初始化dp的第一行,即对于大小为j的背包,用第一个物品能装多满
            if A[0] <= j:
                dp[0][j] = A[0]

        for i in range(1,n):  #循环从第二个物品开始
            for j in range(m+1):
                dp[i][j] = max(dp[i-1][j-A[i]] + A[i],dp[i-1][j])
        return dp[-1][-1]

动态方程由两个循环构成。外循环是物品的index,从1开始,内循环是背包大小,从0开始。这也是背包问题最基本的模板

之所以是物品是外循环是因为每一个物品只能被装一次

但是上面的代码是错误的。原因在于外循环是物品的index,是从1开始的,也就是第2个物品。在计算dp[i-1][j-A[i]]的时候,j-A[i]很可能为负数,导致定位到i-1的最后几列。因此正确的代码为:

    def backPack2(self, m, A):
        #二维度dp
        n = len(A)  #物品的个数
        dp = [[0 for i in range(m+1)] for j in range(n)]  #对于大小为j的背包,前i个物品能装多满
       
        for i in range(m+1):
            if A[0] <= i:
                dp[0][i] = A[0]

        for i in range(1,n):
            for j in range(m+1):
                if A[i] > j:
                    dp[i][j]  = dp[i-1][j]
                else:
                    dp[i][j] = max(dp[i-1][j-A[i]] + A[i],dp[i-1][j])
        return dp[-1][-1]
  • 一维的dp数组

dp[j]表示大小为j的背包最多能装多满。动态方程为:dp[j] = max(dp[j-A[i]] + A[i], dp[j])

需要注意的点在于:

(1)由于是0/1背包,物品是不能重复算的,因此内循环背包的容量采用倒序

(2)内循环背包容量j不能小于A[j],如果小于A[j],会出现表格中右侧的情况(左边是正确代码的情况,以[4,3,2,1],7为例)

    def backPack1(self, m, A):
        #一维度dp
        n = len(A)
        dp = [0] * (m+1)
        for i in range(n):
            for j in range(m, A[i]-1, -1):
                dp[j] = max(dp[j-A[i]] + A[i], dp[j])
        return dp[-1]
     0  1  2  3  4  5  6  7  8  9
4  [0, 0, 0, 0, 4, 4, 4, 4, 4, 4]
3  [0, 0, 0, 3, 4, 4, 4, 7, 7, 7]
2  [0, 0, 2, 3, 4, 5, 6, 7, 7, 9]
1  [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

   0    1     2    3   4   5    6    7    8    9

4 [0,  8,    8,   8,  4,  4,   4,   4,   4,   4]
3 [0, 10, 10,   8,  11, 11, 11,  7,   7,   7]
2 [0, 11,  10, 12, 12, 11, 13, 13, 13,  9]
1 [0, 11,  12, 12, 13, 13, 13, 14, 14, 14]

125. 0/1背包问题 II(有价值)

有 n 个物品和一个大小为 m 的背包. 给定数组 A 表示每个物品的大小和数组 V 表示每个物品的价值。问最多能装入背包的总价值是多大?

样例 1:
输入: m = 10, A = [2, 3, 5, 7], V = [1, 5, 2, 4]
输出: 9
解释: 装入 A[1] 和 A[3] 可以得到最大价值, V[1] + V[3] = 9 

样例 2:
输入: m = 10, A = [2, 3, 8], V = [2, 5, 8]
输出: 10
解释: 装入 A[0] 和 A[2] 可以得到最大价值, V[0] + V[2] = 10

本题与上一题非常相似,一样可以分为二维dp和一维dp的方法,形式基本一样。 

  • 二维的dp数组

dp[i][j]表示对于大小为j的背包,前i个能装入背包的物体所构成的最大价值。

 def backPackII_1(self, m, A, V):
        #二维度dp
        n = len(A)  #物品的个数
        dp = [[0 for i in range(m+1)] for j in range(n)]  #对于大小为j的背包,前i个物品能构成的最大价值
        
        for j in range(m+1):
            if j >= A[0]:
                dp[0][j] = V[0]

        for i in range(1,n):
            for j in range(m+1):
                print(j,A[i])
                if A[i] > j:   #当前物品的重量大于背包重量
                    dp[i][j]  = dp[i-1][j]
                else:
                    dp[i][j] = max(dp[i-1][j-A[i]] + V[i],dp[i-1][j])
        return dp[-1][-1]
  • 一维的dp数组

dp[j]表示大小为j的背包所构成的最大价值。动态方程为:dp[j] = max(dp[j-A[i]] + V[i], dp[j])

    def backPackII_2(self, m, A,V):
        n = len(A)
        dp = [0] * (m+1)
        for i in range(n):
            for j in range(m, A[i]-1, -1):
                dp[j] = max(dp[j-A[i]] + V[i], dp[j])
        return dp[-1]

总结1-1维写法简单总结

1.如果是0-1背包,即数组中的元素不可重复使用,nums放在外循环,target在内循环,且内循环倒序

for i,num in enumerate(nums):
    for j in range(target, num-1, -1):
        dp[j] = max(dp[j-num] + V[i]/num,dp[j])

2.如果是完全背包,即数组中的元素可重复使用,nums放在外循环,target在内循环。且内循环正序

for i,num in enumerate(nums):
    for j in range(num, target+1):
        dp[j] = max(dp[j-num] + V[i]/num,dp[j])

0/1背包问题V-方案个数

给出 n 个物品, 以及一个数组, nums[i] 代表第i个物品的大小, 保证大小均为正数(但是有重复), 正整数 target 表示背包大小, 找到能填满背包的方案数。每一个物品只能使用一次
样例:
给出候选物品集合 [1,2,3,3,7] 以及 target 7
结果的集合为:
[7]
[1,3,3]
返回 2

  • 二维的dp数组

dp[i][j]表示对于大小为j的背包,前i个物品能将背包装满的方案数。

对于第i个物品,有:

  1. 其大小nums[i]>j,说明这个物品一定无法被放入背包,那么其方案数等于前i-1个物品装满背包的方案数:dp[i][j] = dp[i-1][j]
  2. 其大小nums[i]=j,说明这个物品单独就可以放入这个背包并且装满,那么相当于在前i-1个物品装满背包的方案数的基础上又加了1:dp[i-1][j]+1
  3. 其大小nums[i]<j,那么此物品可以放入背包或者不放入背包,放入背包的话其方案数就是dp[i-1][j-nums[i]]。不放入的话其方案数与第一个条件相同dp[i-1][j],故dp[i][j] = dp[i-1][j-nums[i]] + dp[i-1][j]

因此代码如下:

    def backPackV(self, nums, target):
        # write your code here
        nums.sort()
        n = len(nums)
        dp = [[0] * (target+1) for j in range(n)]   #截止到第i个数字,填满大小为j的背包有多少种方案
        for j in range(target+1):
            if j == nums[0]:
                dp[0][j] = 1
        for i in range(1,n):
            for j in range(target+1):
                if nums[i] > j:
                    dp[i][j] = dp[i-1][j]
                elif nums[i] == j:
                    dp[i][j] = dp[i-1][j] + 1
                else:
                    dp[i][j] = dp[i-1][j-nums[i]] + dp[i-1][j]
        return dp[-1][-1]

可以看到上面代码的三个条件都是需要先添加dp[i-1][j],也就是说无论放不放第i个物品,总有dp[i-1][j]个方案,如果进一步放第i个物品,再添加dp[i-1][j-nums[i]]。因此 dp[i][j] = dp[i-1][j-nums[i]] + dp[i-1][j]

但是必须初始化dp[0][0]=1。这里隐含的意思是,假设我们有重量为0的物品,那么对于大小为0的背包,就存在一个方案

def backPackV(self, nums, target):
        nums.sort()
        n = len(nums)
        dp = [[0] * (target+1) for j in range(n)]   #截止到第i个数字,填满大小为j的背包有多少种方案f 
        for j in range(target+1):
            if j == nums[0]:
                dp[0][j] = 1
        dp[0][0] = 1    #必须要有
        for i in range(1,n):
            for j in range(target+1):
                dp[i][j] = dp[i-1][j]
                if nums[i] <= j:
                    dp[i][j] = dp[i][j] + dp[i-1][j-nums[i]]
        return dp[-1][-1]
  • 一维的dp数组

dp[j]表示填满大小为j的背包的方案数。因为物品不能重复使用,因此内循环采用倒序

对于第i个物品,有:

  1. 其大小nums[i]>j,说明这个物品一定无法被放入背包,那么其方案数不变,直接continue(相当于dp[j] = dp[j]
  2. 其大小nums[i]=j,说明这个物品单独就可以放入这个背包并且装满,因此直接在当前的方案数上+1,dp[j] = dp[j] + 1
  3. 其大小nums[i]<j,那么此物品可以放入背包或者不放入背包,放入背包的话其方案数等于背包大小为j-nums[i]的方案数,即dp[j-nums[i]]。不放入的话其方案数与第一个条件相同dp[j],故dp[j] = dp[j-nums[i]] + dp[j]
    def backPackV(self, nums, target):
        # write your code here
        nums.sort()
        n = len(nums)
        dp = [0] * (target+1)  #dp[j]填满大小为j的背包有多少种方案
        for i in range(n):
            for j in range(target,nums[i]-1,-1):
                if nums[i] > j:
                    continue
                elif nums[i] == j:
                    dp[j] = dp[j] + 1
                else:
                    dp[j] = dp[j] + dp[j-nums[i]]
            #print(dp)
        return dp[-1]

同样,上述代码可以进一步简化。这里需要初始化dp[0]=1,其功能就是弥补上述dp[j] = dp[j] + 1。如果j=nums[i],则j-nums[i] = 0dp[0]如果不初始化为1,就会错误。也可以理解为,存在一个大小为0的物品可以装入大小为0的背包,因此方案数为1。

    def backPackV(self, nums, target):
        # write your code here
        nums.sort()
        n = len(nums)
        dp = [0] * (target+1)  #dp[j]填满大小为j的背包有多少种方案
        dp[0] = 1
        for i in range(n):
            for j in range(target,nums[i]-1,-1):
                dp[j] = dp[j] + dp[j-nums[i]]
        return dp[-1]

完全背包问题IV-方案个数

给出 n 个物品, 以及一个数组, nums[i]代表第i个物品的大小, 保证大小均为正数并且没有重复, 正整数 target 表示背包的大小, 
找到能填满背包的方案数。每一个物品可以使用无数次
样例1:输入: nums = [2,3,6,7] 和 target = 7
输出: 2
方案有: 
[7]
[2, 2, 3]

样例2:输入: nums = [2,3,4,5] 和 target = 7
输出: 3
方案有: 
[2, 5]
[3, 4]
[2, 2, 3]

  • 二维的dp数组

dp[i][j]表示对于大小为j的背包,前i个物品能将背包装满的方案数。因为物品使用的次数没有限制,因此设置第三层循环k,表示当前的数字用了几次。

对于第i个物品,有:

  1. 其大小nums[i]>j,说明这个物品一定无法被放入背包,那么其方案数等于前i-1个物品装满背包的方案数:dp[i][j] = dp[i-1][j]
  2. 其大小nums[i]=j,说明这个物品单独就可以放入这个背包并且装满,那么相当于在前i-1个物品装满背包的方案数的基础上又加了1:dp[i-1][j]+1
  3. 其大小nums[i]<j,那么此物品可以放入背包或者不放入背包。放入背包的话,根据放入的个数不同,需要添加相应的方案数,dp[i-1][j-k*nums[i]]。不放入的话其方案数为dp[i-1][j],相当于k=0。因此动态方程为dp[i][j] = dp[i][j] + dp[i-1][j-k*nums[i]]
    def backPackIV(self, nums, target):
        nums.sort()
        n = len(nums)
        dp = [[0] * (target+1) for j in range(n)]   #截止到第i个数字,填满大小为j的背包有多少种方案
        for j in range(1,target+1): 
            if j % nums[0] == 0:
                dp[0][j] = 1             
        
        for i in range(1,n):
            for j in range(target+1): 
                print(nums[i],j)
                if nums[i] > j:   #背包不能放入该物品
                    dp[i][j] = dp[i-1][j]
                elif nums[i] == j:   #背包放入该物品(放一个)
                    dp[i][j] = dp[i-1][j] + 1
                else:    #背包放入该物品的个数为k
                    times = j // nums[i]
                    for k in range(0,times+1):
                        dp[i][j] = dp[i][j] + dp[i-1][j-k*nums[i]]
        return dp[-1][-1]

但是上述代码是有一点问题的,如下。我们默认从1开始循环,也就是说对于大小为0的背包是不考虑的。但是实际上对于大小为0的背包,我们不放入任何物品,就是一个方案,即dp[0][0] = 1

        for j in range(1,target+1): 
            if j % nums[0] == 0:
                dp[0][j] = 1  

 例如给定物品[2,3,5],背包的大小为6,那么我们放三个大小为2的物品是一个方案,放两个大小为3的的物品也是一个方案。在dp第二行计算dp[3][6]的时候(这里直接以物品的大小作为行的index),dp[3][6] = dp[2][6] + dp[2][0],由于默认从1开始循环, dp[2][0]=0,因此dp[3][6] = 1 + 0 = 1,而实际上dp[3][6] =2,就出错了。

从0开始循环(正确)从1开始循环(错误)
0  1  2  3  4  5  6
2 [1, 0, 1, 0, 1, 0, 1]
3 [1, 0, 1, 1, 1, 1, 2]
5 [1, 0, 1, 1, 1, 2, 2]
   0  1  2  3  4  5  6
2 [0, 0, 1, 0, 1, 0, 1]
3 [0, 0, 1, 1, 1, 1, 1]
5 [0, 0, 1, 1, 1, 2, 1]

此外实际上通过上面的分析,if elif else可以合并为一个,k等于0就是不放入,k=1就是放入一个,以此类推,因此可以合并。

    def backPackIV(self, nums, target):
        nums.sort()
        n = len(nums)
        dp = [[0] * (target+1) for j in range(n)]   #截止到第i个数字,填满大小为j的背包有多少种方案
        for j in range(target+1):  #从0开始循环
            if j % nums[0] == 0:
                dp[0][j] = 1             
        for i in range(1,n):       
            for j in range(target+1):
                times = j // nums[i]
                for k in range(0,times+1):
                    dp[i][j] = dp[i][j] + dp[i-1][j-k*nums[i]]
        return dp[-1][-1]
  • 一维的dp数组

dp[j]表示填满大小为j的背包的方案数。由于物品是可以重复选取的,因此内循环的顺序的正序的

对于第i个物品,有:

  1. 其大小nums[i]>j,说明这个物品一定无法被放入背包,那么其方案数不变,直接continue(相当于dp[j] = dp[j]
  2. 其大小nums[i]=j,说明这个物品单独就可以放入这个背包并且装满,因此直接在当前的方案数上+1,dp[j] = dp[j] + 1
  3. 其大小nums[i]<j,那么此物品可以放入背包或者不放入背包。不放入的话其方案数就等于dp[j],无需增加,因此k从1开始循环。放入背包的话,根据放入的个数不同,需要添加相应的方案数,dp[i-1][j-k*nums[i]]。因此动态方程为dp[j] = dp[j] + dp[j-k*nums[i]]
    def backPackIV(self, nums, target):
        nums.sort()
        n = len(nums)
        dp = [0] * (target + 1)
        for i in range(n):
            for j in range(target+1): 
                if nums[i] > j:##背包不能放入该物品 dp[j] = dp[j]
                    continue
                elif nums[i] == j:  #背包放入该物品(放一个)
                    dp[j] = dp[j] + 1
                else:  
                    times = j // nums[i]
                    for k in range(1,times+1):
                        dp[j] = dp[j] +  dp[j-k*nums[i]]
        return dp[-1]

这个代码看似没有问题,但实际上是错误的,仍以给定物品[2,3,5],背包的大小为6为例。上述代码生成的dp矩阵如下,可以看出当i=0,即nums[i] = 2时,大小为6的背包填满的方案只有一种,就是2,2,2,即dp[6] = 1。但是由于引入了k,当k=1时,dp[6] = dp[6] + dp[4] = 0 + 1 = 1.当k = 2时,dp[6] = dp[6] + dp[2] = 1 + 1 = 2,等于多算了一次。

     0  1  2  3  4  5  6
2  [0, 0, 1, 0, 1, 0, 2]
3  [0, 0, 1, 1, 1, 1, 3]
5  [0, 0, 1, 1, 1, 2, 3]

因此这里直接去掉k循环即可,即:

        for i in range(n):
            for j in range(target+1): 
                if nums[i] > j:##背包不能放入该物品 dp[j] = dp[j]
                    continue
                elif nums[i] == j:  #背包放入该物品(放一个)
                    dp[j] = dp[j] + 1
                else:  
                    dp[j] = dp[j] +  dp[j-nums[i]]

进一步地,可以将if elif else做一个合并。这里需要初始化dp[0]=1,其功能就是弥补上述dp[j] = dp[j] + 1

    def backPackIV(self, nums, target):
        n = len(nums)
        dp = [0 for _ in range(target+1)]
        dp[0] = 1
        for i in range(n):
            for j in range(nums[i],target+1):
                dp[j] = dp[j - nums[i]] + dp[j]
        return dp[-1]

方案数

0/1背包(要么放入背包,要么不放入)

完全背包(可以多次选取)

二维

dp[i][j]对于前i个物品,装满大小为j的背包的方案数

外循环物品,内循环背包大小(正序)

dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]]              =  不放入背包   +  放入背包

外循环物品,内循环背包大小(正序)

dp[i][j] = dp[i][j] + dp[i-1][j-k*nums[i]]

k = 0:不放入背包

k > 0:  放入背包

dp[j]填满大小为j的背包的方案数

(1)外循环物品,内循环背包大小(倒序

(2)初始化dp[0]=1

dp[j] = dp[j] + dp[j-nums[i]]

          =    不放入背包   +  放入背包

(1)外循环物品,内循环背包大小(正序

(2)初始化dp[0]=1

dp[j] = dp[j] + dp[j - nums[i]]

            =    不放入背包   +  放入背包

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值