背包问题之模板题 Python实现
前言
01背包——万恶之源
我一定要搞好这个背包问题!
一、 01背包
1. 问题描述
01背包问题:给定\(N\)个物品和容量为\(V\)的背包,每个物品有两个属性:价值\(w_i\)和体积\(v_i\),每个物品只能取1次,问在背包中放入哪些物品可以使得总价值最大?
输入例子:
4 5 # 物品数量和背包容量
1 2 # 物品1的体积和价值
2 4
3 4
4 5
输出例子:
8 # 价值最大的结果
2. 解题思路
- 状态表示f[i, j](一般背包问题都是二维)
- 表示所有选法,满足只从前i个物品中选,并且选出来的物品总体积小于等于j,f[i, j]的值为这些选法的价值的最大值
- 前i个物品:这个价值最大值从前i个物品考虑,并不代表前i个物品都选了
- 最后答案是\(f[N][V]\)
- 状态计算
-
对于f[i, j],可以将该集合划分为两部分:选法包含i,选法不包含i。两部分的最大值就是f[i, j]
-
如何计算这两部分
-
选法不包含i:从1~i-1中选,且总体积不超过j的所有选法的最大值(这些选法一定不会包含i),即f[i-1, j]
-
选法包含i:包含i的选法的最大值
如何求?——“曲线救国”
-
先不去看第i个物品,即从第1~i-1个物品中选。
但是由于最后要加上第i个物品,所以在第1到i-1个物品选时,选法不能超过\(j - v[i]\),得出这部分最大值,即\(f[i-1, j - v[i]]\)
-
再加上第i个物品的价值,即可得到包含i的选法的最大价值
即\(f[i-1, j - v[i]] + w[i]\)
-
-
-
注意,当\(j < v_i\)的时候,即选法总体积装不下第i个物品的时候,选法包含i这个情况不存在
💡 总结
遍历所有物品
遍历所有体积
\(f[i, j] = max(f[i - 1, j], f[i-1, j-v[i]] + w[i])\),其中\(f[i-1, j-v[i]] + w[i]\)只有当\(j >= v[i]\)的时候才存在
3. 代码实现
3.1 朴素版
\(O(NV)\)
f = [[0 for _ in range(N+1)] for _ in range(V+1)]
for i in range(1, N+1):
for j in range(1, V+1):
f[i][j] = f[i-1][j]
if j >= v[i]:
f[i][j] = max(f[i][j], f[i-1][j-v[i]] + w[i])
print(f[N][V])
3.2 压缩版
\(O(NV)\)
原理是:
- f[i]层只用到了f[i-1],因此可以把 i 这一维去掉
- 计算f[i, j]用到的j(如\(f[i-1, j],f[i-1, j-v[i]]\))都是\(≤j\),所以如果从大到小遍历所有体积,用到 j 前面的都是从上一层更新完毕的,即都为f[i-1]的
- f[i, j] = f[i-1, j]可以省略,其实 f[i-1, j]就是f[i]在计算之前的值,f[i, j]是f[i]计算之后的值
f = [0 for _ in range(V + 1)]
for i in range(1, N + 1):
for j in range(V, v[i]-1, -1):
f[j] = max(f[j], f[j - v[i]] + w[i])
二、完全背包
1. 问题描述
在01背包问题的基础上,考虑的是每个物品可以取无限次
2. 解题思路
- 状态表示f[i, j](一般背包问题都是二维)
- 表示所有选法,满足只从前i个物品中选,并且选出来的物品总体积小于等于j,f[i, j]的值为这些选法的价值的最大值
- 前i个物品:这个价值最大值从前i个物品考虑,并不代表前i个物品都选了
- 最后答案是\(f[N][V]\)
- 状态计算
-
对于f[i, j],可以将该集合划分为k部分:第i个物品选0个(即不包含i)、第i个物品选1个,...,第i个物品选k个(不可能无限,因为物品有体积,背包也有大小),这些部分的最大值就是f[i, j]
-
如何计算这些部分
-
第i个物品选k个:
如何求?——“曲线救国”
-
先不去看k个第i个物品,即从第1~i-1个物品中选。
但是由于最后要加上第i个物品,所以在第1到i-1个物品选时,选法不能超过\(j - k * v[i]\),得出这部分最大值,即\(f[i-1, j - k* v[i]]\)
-
再加上k个第i个物品的价值,即可得到包含i的选法的最大价值
即\(f[i-1, j - k * v[i]] + k * w[i]\)
-
-
-
k的取值是0,最大值应该是k * v[i] <= j
💡 总结
遍历所有物品
遍历所有体积
遍历所有个数
计算\(f[i,j]= max(f[i-1, j - k * v[i]] + k * w[i])\)
3. 代码实现
3.1 朴素版
\(O(NVK)\)
f = [[0 for _ in range(N+1)] for _ in range(V+1)]
for i in range(1, N+1):
for j in range(1, V+1):
k = 0
while k * v[i] <= j:
f[i][j] = max(f[i][j], f[i-1][j-k * v[i]] + k * w[i])
print(f[N][V])
3.2 压缩版
\(O(NV)\)
原理:
- 将k展开,对比f[i, j]和f[i, j - v](v简化v[i])
可以发现,红色部分和橙色部分相比,每一项都多了个w,因此,f[i, j]的计算可以简化成:
\(f[i][j] = max(f[i-1][j], f[i][j-v[i]]+w[i])\)
-
对比01背包的方程
01背包:\(f[i][j] = max(f[i-1][j], f[i-1][j-v[i]]+w[i])\)
完全背包:\(f[i][j] = max(f[i-1][j], f[i][j-v[i]]+w[i])\)
可以发现,01背包是从上一层转换过来的,完全背包是从当层转移过来的,所以可以从小到大枚举所有体积,这样当算到 j 时,前面的 j-v[i] 已经被计算过了,即属于当层转移
f = [0 for _ in range(V + 1)]
for i in range(1, N + 1):
for j in range(v[i], V + 1):
f[j] = max(f[j], f[j - v[i]] + w[i])
三、多重背包
1. 问题描述
在完全背包问题的基础上,考虑的是每个物品可以取\(S_i\)次,即会给每个物品次数的限制
2. 解题思路
多重背包可以直接在完全背包的基础上,将k(物品取的次数)的条件加上 <= s[i] 即可
3. 代码实现
3.1 朴素版
\(O(NVS)\)
f = [[0 for _ in range(N+1)] for _ in range(V+1)]
for i in range(1, N+1):
for j in range(1, V+1):
k = 0
while k <= s[i] && k * v[i] <= j:
f[i][j] = max(f[i][j], f[i-1][j-k * v[i]] + k * w[i])
print(f[N][V])
4. 多重背包的优化--二进制优化
\(O(NVlogS)\)
4.1 思路
-
借鉴完全背包的思想,对比f[i, j] 和 f[i, j-v]
-
可以发现,f[i, j - v]比f[i, j]的绿色部分多了f[i - 1, j - (s + 1)v] + sw,也就是说,已知红色部分的最大值,能不能推出绿色部分的最大值?
可以将问题看成,已知红色部分的最大值和f[i - 1, j - (s + 1)v] + sw,可不可以推出绿色部分,即已知 max(a, b, c)和 c 的值,求max(a, b)。
-
已知 max(a, b, c)和 c 的值,max(a, b)是无法确定。原因如下:
- 如果max(a, b, c) = a/b,那么max(a, b) =a/b可以确定
- 如果max(a, b, c) = c,那么max(a, b) = ?从已知条件是推不出的
-
因此,不能直接用完全背包的优化来优化多重背包
- 二进制优化
假设s = 1023,即对于每一个物品共有0~1023种选法,将选法根据二进制打包成k组,每一组包含选取\(2^i\)个物品,然后每一组最多只能取一次。这样0~1023中每一种选法都可以用k组的不同组合来表示。
k组:1, 2, 4, 8,...,512
则 考虑第一组,可以取0~1个
加上第二组,则可以取 2~3个,加起来就是取0~3个
加上第三组,则可以取 4~7个,加起来就是取0~7个
以此类推,取0~1023都可以用这些组来表示
-
如果s不能完整分为k组,则最后一组设为\(C = s - \sum_1^{k-1} 2^i\)
即S可以分成k组,\(1, 2, 4, ..., 2^{k-1},C \quad and \quad C < 2^{k}\)
因此,我们可以将多重背包转换成01背包问题,每一组只有取或者不取
4.2 代码实现
n, m = tuple(map(int,input().split())) # 物品总量和背包容量
v = []
w = []
for i in range(n):
# 对于每一个物品
a, b, s = tuple(map(int,input().split()))
k = 1 # 对k个物品进行打包
while k <= s:
v.append(a * k) # 把k个物品i打包在一起
w.append(b * k)
s -= k
k *= 2
if s > 0: # 说明s不能被k减完,还有剩的C
v.append(a * s)
w.append(b * s)
n = len(v) # 重新更新物品数量,为新打包的数量
# 01背包
f = [0] * (m+1)
for i in range(1, n + 1):
for j in range(m, v[i] - 1, -1):
f[j] = max(f[j], f[j - v[i]] + w[i])
print(f[m])
四、分组背包
1. 问题描述
在01背包问题的基础上,考虑的是每个物品属于不同的分组,每一组只能选1个物品
输入示例:
3 5 # 物品组数N和背包容量V
# 接下来有N组数据
2 #第i组有s个物品
1 2 #第i组第j个物品的体积和价值
2 4
1
3 4
1
4 5
输出示例:
8
2. 解题思路
每一组只能取一次,其实就有01背包的影子了:
- v、w设成二维的,v[i][j] 表示第i组第j个物品的体积
- 多一层循环遍历每一组的每一个物品,只有它的体积 <= j 时,就可以用转移方程(因此,特判的不是组别的体积,所以第二层循环的 j 从 V 到 0 遍历)
3. 代码实现
for i in range(1,n+1):
for j in range(m, -1, -1):
for k in range(s):
if v[i][k] <= j:
f[j] = max(f[j], f[j - v[i][k]] + w[i][k])