void CompletePack(int C, int N, int W[], int V[]) {
memset(dp, 0, sizeof (dp));
for (int i = 1; i <= N; ++i) {
for (int j = w[i]; j <= C; ++j)
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
}
2 完全背包问题
2.1 题目
有 N 种物品和一个容量为 V 的背包,每种物品都有无限件可用。放入第 i 种
物品的耗费的空间是 C i ,得到的价值是 W i 。求解:将哪些物品装入背包,可使
这些物品的耗费的空间总和不超过背包容量,且价值总和最大。
2.2 基本思路
这个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就是从
每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取 0 件、
取 1 件、取 2 件……直至取 ⌊V /C i ⌋ 件等很多种。
4
如果仍然按照解01背包时的思路,令 F[i,v] 表示前 i 种物品恰放入一个容量
为 v 的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方
程,像这样:
F[i,v] = max {F[i − 1,v − kC i ] + kW i | 0 ≤ kC i ≤ v}
这跟01背包问题一样有 O(V N) 个状态需要求解,但求解每个状态的时
间已经不是常数了,求解状态 F[i,v] 的时间是 O(
v
C i ) ,总的复杂度可以认为
是 O(NV Σ
V
C i ) ,是比较大的。
将01背包问题的基本思路加以改进,得到了这样一个清晰的方法。这说明01
背包问题的方程的确是很重要,可以推及其它类型的背包问题。但我们还是要
试图改进这个复杂度。
2.3 一个简单有效的优化
完全背包问题有一个很简单有效的优化,是这样的:若两件物品 i 、 j 满
足 C i ≤ C j 且 W i ≥ W j ,则将可以将物品 j 直接去掉,不用考虑。
这个优化的正确性是显然的:任何情况下都可将价值小耗费高的 j 换成物美
价廉的 i ,得到的方案至少不会更差。对于随机生成的数据,这个方法往往会大
大减少物品的件数,从而加快速度。然而这个并不能改善最坏情况的复杂度,
因为有可能特别设计的数据可以一件物品也去不掉。
这个优化可以简单的 O(N 2 ) 地实现,一般都可以承受。另外,针对背包
问题而言,比较不错的一种方法是:首先将费用大于 V 的物品去掉,然后使
用类似计数排序的做法,计算出费用相同的物品中价值最高的是哪个,可
以 O(V + N) 地完成这个优化。这个不太重要的过程就不给出伪代码了,希望你
能独立思考写出伪代码或程序。
2.4 转化为01背包问题求解
01背包问题是最基本的背包问题,我们可以考虑把完全背包问题转化为01背
包问题来解。\
最简单的想法是,考虑到第 i 种物品最多选⌊V/C i⌋件,于是可以把第i种物品转化为⌊V/C i⌋
件费用及价值均不变的物品,然后求解这个01背包问题。这样的做法
完全没有改进时间复杂度,但这种方法也指明了将完全背包问题转化为01背包
问题的思路:将一种物品拆成多件只能选 0 件或 1 件的01背包中的物品。
更高效的转化方法是:把第 i 种物品拆成费用为 C i 2 k 、价值为 W i 2 k 的若干件
物品,其中 k 取遍满足 C i 2 k ≤ V 的非负整数。
这是二进制的思想。因为,不管最优策略选几件第 i 种物品,其件数写成
二进制后,总可以表示成若干个 2 k 件物品的和。这样一来就把每种物品拆
成 O( log
V
C i ) 件物品,是一个很大的改进。
2.5 O(V N) 的算法
这个算法使用一维数组,先看伪代码:
F[0..V ] = 0
for i = 1 to N
for v = C i to V
F[v] = max (F[v],F[v − C i ] + W i )
5
你会发现,这个伪代码与01背包问题的伪代码只有 v 的循环次序不同而已。
为什么这个算法就可行呢?首先想想为什么01背包中要按照 v 递减的次序来
循环。让 v 递减是为了保证第 i 次循环中的状态 F[i,v] 是由状态 F[i − 1,v − C i ] 递
推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入
第 i 件物品”这件策略时,依据的是一个绝无已经选入第 i 件物品的子结果 F[i −
1,v − C i ] 。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加
选一件第 i 种物品”这种策略时,却正需要一个可能已选入第 i 种物品的子结
果 F[i,v − C i ] ,所以就可以并且必须采用 v 递增的顺序循环。这就是这个简单的
程序为何成立的道理。
值得一提的是,上面的伪代码中两层for循环的次序可以颠倒。这个结论有
可能会带来算法时间常数上的优化。
这个算法也可以由另外的思路得出。例如,将基本思路中求解 F[i,v − C i ] 的
状态转移方程显式地写出来,代入原方程中,会发现该方程可以等价地变形成
这种形式:
F[i,v] = max (F[i − 1,v],F[i,v − C i ] + W i )
将这个方程用一维数组实现,便得到了上面的伪代码。
最后抽象出处理一件完全背包类物品的过程伪代码:
def CompletePack( F,C,W )
for v = C to V
F[v] = max {F[v],f[v − C] + W}
2.6 小结
完全背包问题也是一个相当基础的背包问题,它有两个状态转移方程。希
望你能够对这两个状态转移方程都仔细地体会,不仅记住,也要弄明白它们是
怎么得出来的,最好能够自己想一种得到这些方程的方法。
事实上,对每一道动态规划题目都思考其方程的意义以及如何得来,是加
深对动态规划的理解、提高动态规划功力的好方法。\