完全背包
完全背包问题与01背包问题背景类似, 同样是有 N 个物品分别具有自己的体积
v[i]
和价值w[i]
. 但不同的是, 完全背包中的每一个物品都可以无限次被取用, 没有次数限制.
基础思路
首先从状态转移的基础思路出发, 这道题和 01 背包有很多的相似之处. 我们可以借鉴 01 背包状态函数的设计思路, 同时在这个过程中注意两者的关键不同: 物品的多次使用. 物品能够多次被使用, 意味着我们在计算一个函数的状态的时候, 不仅仅需要思考这个物品是否被使用, 还需要思考这个物品被使用了几次.
直观的思路是在原始的二重循环物品 i 和占用了的体积 j 之外再使用一个变量 k 来遍历每个物品使用的次数.
for i in range(N):
for j in range(V):
while k*v[i] + j <= V:
f[i][j] = max(f[i-1][j], f[i][j-k*v[i]] + k*w[i])
k += 1
但显而易见的, 这样的算法复杂度为 O(NV*V/v[i])
, 需要进一步优化. 优化的方法不止一种, 比如 背包九讲 中提到的使用二进制思想优化, 再比如即将介绍的一维数组的优化. 这里笔者直接介绍平时使用的, 时间复杂度最低的方法, 做详细解释.
从 01 背包出发的一维简化
首先来看看 01 背包的一维数组状态转移的写法:
for i in range(N):
for j in range(V+1)[::-1]: # 注意这个地方的倒序循环
f[j] = max(f[j], f[j-w[i]] + v[i])
这里复习一下, 当时使用倒序遍历 j 的原因是什么呢? 是我们希望在计算每一次 f[j]
的时候, 都能够保证 f[j-w[i]]
没有刷新. 为什么我们希望没有刷新呢? 因为我们希望保证这个时候的状态函数一定是未曾使用过第 i 件物品的状态函数, 从而满足 01 背包的限制: 每个物品只能使用一次.
这意味着什么? 意味着如果我们希望能够重复计算每个物件, 让它们能够被多次使用, 那么我们只需要把遍历顺序正过来!
for i in range(N):
j = v[i]
while j >= v[i] and j <= m:
f[j] = max(f[j], f[j-v[i]] + w[i])
j += 1
这样就可以解决完全背包问题, 同时能够保证更小的时间和空间复杂度.
这样的论证缺乏足够的说服力, 接下来使用数学归纳法将这个状态转移的过程能够解决完全背包问题证明一下.
- 初始状态下, 第一件物品时, 能够保证
f[j](1)
是此时的最优解, 这个是显而易见的. - 假设在计算第 i-1 件物品时,
f[j]
是最优解. 这里为了区分物品遍历时的次数, 将状态函数写为f[j](i-1)
- 第 i 件物品时:
f[j](i) = max(f[j](i-1), f[j-v[i]](i) + w[i])
- 从最小开始遍历:
f[v[i]](i) = max(f[v[i]](i-), f[0] + w[i])
, 这个时候显而易见我们一定会得到对于体积为v[i]
的情况下的最优解. - 此时,
f[j](i-1)
根据前面的假设, 已经是不添加第 i 件物品的情况下, 体积为 j 时的最优解, 而f[j-v[i]](i) + w[i]
也是添加 i 物品的情况下, 体积为 j 的最优解, 因为此时我们是从小向大遍历, 我们已经保证了f[j-v[i]](i)
是体积为j-v[i]
时, 加入第 i 件物品情况下的最优解.
- 从最小开始遍历:
所以完全背包的这种算法被证明.
Python 题解
笔者水平有限, 代码仅供参考.
N, V = map(int, input().split())
v, w = [], []
for _ in range(N):
t1, t2 = map(int, input().split())
v.append(t1)
w.append(t2)
f = [0]*(V+1)
for i in range(N):
j = v[i]
while j<= V:
f[j] = max(f[j], f[j-v[i]] + w[i])
j += 1
print(f[V])