0-1 背包
每种物品仅有一件, 可以选择放或者不放 (01). 然后通过计算每个物品的花费, 来得到一定费用限制下能够取到的最大物品价值
基础思路
如上图所说, 设计一个状态函数来表征当前情况的状态: f[i][j]
表示在装入了 i 个物品, 占用了 j 的容量(花费了 j 数量的费用) 后物品的最大和价值. 在这个情况下, 分别用 v[i]
和 w[i]
来储存第 i 个物品的价值和需要花费的容量.
通过这个状态函数的转移, 来获得最终的状态函数.
- 状态转移方程的构建思路是: 从当下的 i 个物品状态下转移到 i+1 个物品的状态下时有两个选择
- 往背包内装入新物品, 那么需要保证之前的 i 个物品都存放在
j-w[i]
的容量中:f[i-1][j-w[i]] + v[i]
- 不放入新物品:
f[i-1][j]
- 往背包内装入新物品, 那么需要保证之前的 i 个物品都存放在
这样的话, 我们就能从 0 个物品时的价值出发, 一步步获得有 N 个物品的情况下, 最大的价值: max(f[N][j]
.
前面的步骤完成了在物品数量上的状态转移, 因此我们能够直接通过求取最后目标数量 N 的物品个数时状态函数的最大值来获取最终我们想要得到的目标.
但这个数值并不总是等同于 f[N][V]
, 即不一定只有在背包完全装满的时候取到最大值. 如果希望最后直接通过调用 f[N][V]
来得到最终的最大价值, 我们在完成物品数量 i 的状态转移的同时, 还需要完成背包容量 j 的状态转移.
要进背包容量的状态转移, 只需要在状态转移方程中加上一项 f[i][j-1]
即可. 这样就可以在背包容量未满时取到最大价值的情况, 一样能转移到最终的 f[N][V]
的状态函数上.
最终的状态转移方程即为:
f[i][j] = max(f[i][j-1], f[i-1][j], f[i-1][j-w[i]]+v[i])
空间复杂度的优化
前边使用了一个二维数组来储存 i 个物品, 使用了 j 容量的情况下的状态函数. 但其实我们需要求的使用的物品数量是固定的, 也就是说我们只需要最后一个状态 f[N][V]
. 那么有没有办法能够在我们只用一个一维数组, 仅仅储存容量为 j 时物品的总价值, 而将物品数量的状态转移暗含在循环的行进过程中, 从而能够减少空间复杂度呢?
我们先看之前的二维数组情况下的遍历逻辑:
for i in range(1, N+1):
for j in range(0, V+1):
f[i][j] = max(f[i-1][j], f[i-1][j-w[i]] + v[i])
这个时候是只考虑了物品数量的状态转移. 我们将这个循环优化为一维数组的循环, 如下图所示.
for i in range(N):
for j in range(V+1)[::-1]: # 注意这个地方的倒序循环
f[j] = max(f[j], f[j-w[i]] + v[i])
这里的优化过程中最精彩的部分就是第二个循环的修改. 为什么要这么做呢? 我们可以思考一下.
原始的状态转移方程是由两个在 i-1
的状态下的状态函数转移到 i
的状态下的状态函数. 那么我们在一维数组做循环的时候, 就必须要强调状态转移方程的右侧全部都还处于 i-1 的循环中. 这就意味着我们需要保证在进行每一轮的状态函数的刷新的时候, f[j-w[i]]
是在 f[j]
之后刷新的, 所以我们需要从大到小遍历进行刷新!
另外, 可能有人会注意到, 之前的时候为了保证容量也跟着一直在刷新, 加入了一个 f[i][j-1]
, 而在简化空间复杂度后, 这一项去掉之后仍然没有发现问题, 那么是为什么呢? 笔者暂时也还没有想的完全通透, 但是能肯定的是, 直接简单的添加一项 f[j-1]
是一定不可行的. 因为我们是从大向小遍历, 这种情况下的 f[j-1]
是还未刷新的, 等价于二维数组中的 f[i-1][j-1]
, 所以这样是一定不可行的. 而不添加的可行性等我想明白了再进行补充.
Python 题解
python的循环逻辑和输入逻辑个人感觉都比较傻逼一点, 所以写起来是真费劲呐. 笔者水平有限, 代码仅供参考.
v, w = [], []
n, m = map(int, input().split())
for _ in range(n):
t1, t2 = map(int, input().split())
v.append(t1)
w.append(t2)
f = [0]*(m+1)
for i in range(n):
j = m
while j >= v[i] and j <= m:
f[j] = max(f[j], f[j-v[i]] + w[i])
j -= 1
print(f[m])