目录
92. 0/1背包问题(无价值)
125. 0/1背包问题 II(有价值)
总结1
0/1背包问题V-方案个数
完全背包问题IV-方案个数
在n个物品中挑选若干物品装入背包,最多能装多满?假设背包的大小为m,每个物品的大小为A[i]
样例 1:
输入: [3,4,8,5], backpack size=10
输出: 9
样例 2:
输入: [2,3,5,7], backpack size=12
输出: 12
表示对于大小为的背包,前个物品能装多满。所谓0/1背包问题,就是对于某一个物品,它有两种状态,要么放入背包,状态为1,要么不放入背包,状态为0,因此可分两种情况:
(1)第个物品没有放入背包中,那么此时背包装的大小为。
(2)第个物品放入背包中,那么此时背包装的大小为。
由于我们求得是最多能装多满,因此是最大值,即
因此代码如下:
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个物品。在计算的时候,很可能为负数,导致定位到的最后几列。因此正确的代码为:
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]
表示大小为的背包最多能装多满。动态方程为:。
需要注意的点在于:
(1)由于是0/1背包,物品是不能重复算的,因此内循环背包的容量采用倒序。
(2)内循环背包容量不能小于,如果小于,会出现表格中右侧的情况(左边是正确代码的情况,以[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] |
有 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的方法,形式基本一样。
表示对于大小为的背包,前个能装入背包的物体所构成的最大价值。
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]
表示大小为的背包所构成的最大价值。动态方程为:。
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.如果是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])
给出 n 个物品, 以及一个数组, nums[i] 代表第i个物品的大小, 保证大小均为正数(但是有重复), 正整数 target 表示背包大小, 找到能填满背包的方案数。每一个物品只能使用一次。
样例:
给出候选物品集合 [1,2,3,3,7] 以及 target 7
结果的集合为:
[7]
[1,3,3]
返回 2
表示对于大小为的背包,前个物品能将背包装满的方案数。
对于第i个物品,有:
因此代码如下:
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[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]
表示填满大小为的背包的方案数。因为物品不能重复使用,因此内循环采用倒序。
对于第i个物品,有:
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]
同样,上述代码可以进一步简化。这里需要初始化,其功能就是弥补上述。如果,则,如果不初始化为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]
给出 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]
表示对于大小为的背包,前个物品能将背包装满的方案数。因为物品使用的次数没有限制,因此设置第三层循环k,表示当前的数字用了几次。
对于第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]
表示填满大小为的背包的方案数。由于物品是可以重复选取的,因此内循环的顺序的正序的。
对于第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做一个合并。这里需要初始化,其功能就是弥补上述
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的背包的方案数 |
外循环物品,内循环背包大小(正序) = 不放入背包 + 放入背包 |
外循环物品,内循环背包大小(正序) k = 0:不放入背包 k > 0: 放入背包 |
dp[j]填满大小为j的背包的方案数 |
(1)外循环物品,内循环背包大小(倒序) (2)初始化dp[0]=1 = 不放入背包 + 放入背包 |
(1)外循环物品,内循环背包大小(正序) (2)初始化dp[0]=1 = 不放入背包 + 放入背包 |