完全背包问题
题目
有N种物品和一个容量为V的背包, 每种物品都有无限件可用. 第i种物品的费用是 c [ i ] c[i] c[i], 价值为 w [ i ] w[i] w[i]. 求解那些物品装入 背包可使这些物品的费用综合不超过背包容量, 且价值总和最大
基本思路
这个问题非常类似与01背包问题, 所不同的是每种物品有无限件, 也就是从每种物品的角度考虑, 与它相关的策略已并非取或不取两种, 而是 有取0件, 1件, 2件, ⋯ ⋯ \cdots \cdots ⋯⋯等很多种, 如果仍然按照01背包时的思路, 令 f [ i ] [ v ] f[i][v] f[i][v]表示前 i i i种物品恰放入一个容量为v的背包的最大权值, 仍然可以按照每种物品不同的策略写出状态转移方程, 像如下这样: f [ i ] [ v ] = m a x { f [ i − 1 ] [ v ] , f [ i − 1 ] [ v − k ⋅ c [ i ] ] + k ⋅ w [ i ] } , 0 ≤ k ⋅ c [ i ] ≤ v f[i][v]=max\{f[i-1][v], f[i-1][v-k\cdot c[i]]+k\cdot w[i]\}, 0\le k\cdot c[i]\le v f[i][v]=max{f[i−1][v],f[i−1][v−k⋅c[i]]+k⋅w[i]},0≤k⋅c[i]≤v 这跟01背包问题一样有 O ( N × V ) O(N\times V) O(N×V)个状态需要求解, 但求解每个状态的时间已经不是常数了, 求解状态 f [ i ] [ v ] f[i][v] f[i][v]的时间是 O ( v c [ i ] ) O(\frac{v}{c[i]}) O(c[i]v), 总的复杂度是超过 O ( N × V ) O(N\times V) O(N×V)的. 该算法伪代码如下所示:
for i=1..N:
for v=0..V:
for k=1..v/c[i]:
f[v]=max{f[v], f[v-k*cost]+k*weight}
将01背包问题的基本思路加以改进, 得到了这样一个清晰的方法. 这说明01背包问题的方程的确很重要, 可以推及其他类型的背包问题. 但这里试图改进这个算法的复杂度.
一个简单有效的优化
-
完全背包问题有一个很简单有效的优化, 是这样的: 若两件物品 i i i、 j j j满足 c [ i ] ≤ c [ j ] c[i]\le c[j] c[i]≤c[j]且 w [ i ] ≥ w [ j ] w[i]\ge w[j] w[i]≥w[j],则将物品 j j j去掉, 不用考虑.
-
这个优化的的正确性显然: 任何情况下都可件价值小费用高的 i i i换成物品价廉的i, 这样得到的方案至少不会更差的方案. 对于随机生成的数据, 这个方法往往会大大减少物品的件数, 从而加快速度,
-
这个并不能改善最坏情况的复杂度, 因为有可能特别设计的数据可以一件物品也去不掉
-
这个优化可以简单的 O ( N 2 ) O(N^2) O(N2)地实现, 一般都可以承受.
-
另外, 针对背包问题而言, 比较不错的一种方法是: 首先件费用大于 V V V的物品去掉, 然后使用类似计数排序的做法, 计算出费用相同的物品中价值最高的是哪个, 可以 O ( V + N ) O(V+N) O(V+N)地完成这个优化.
转化为01背包问题求解
既然01背包问题是最基本的背包问题, 那么我们可以考虑把完全背包问题转换为01背包问题来解.
-
最简单的想法是, 考虑到第i种物品最多选 V c [ i ] \frac{V}{c[i]} c[i]V件, 于是可以把第i中物品转换为 V c [ i ] \frac{V}{c[i]} c[i]V件费用及价值均不变的物品, 然后求解这个01背包问题. 这样完全没有改进基本思路的时间复杂度, 但这毕竟给了我们件完全背包问题转化为01背包问题的思路: 将一种物品拆成多件物品
-
更高效的转换方法是: 把第i种物品拆成费用为 c [ i ] ⋅ 2 k c[i]\cdot 2^k c[i]⋅2k、价值为 w [ i ] ⋅ 2 k w[i]\cdot 2^k w[i]⋅2k的若干件物品, 其中 k k k满足 c [ i ] ⋅ 2 k ≤ V c[i]\cdot 2^k\le V c[i]⋅2k≤V. 这是二进制的思想, 因为不管最优策略选集中第 i i i种物品, 总可以表示成若干个 2 k 2^k 2k件物品的和, 这样把每种物品拆成 O ( log ( V c [ i ] ) ) O(\log(\frac{V}{c[i]})) O(log(c[i]V))件物品, 是一个很大的改进.
但是我们有更优的 O ( V × N ) O(V\times N) O(V×N)的算法
O ( V × N ) O(V\times N) O(V×N)的算法
这个算法使用一维数组, 先看伪代码:
for i=1..N:
for v=0..V:
f[v]=max{f[v], f[v-cost]+weight}
发现上述代码与01背包问题的伪代码只有 v v v的循环次序不同而已, 为什么这样一改就可行呢?
-
首先, 想想为什么01背包问题中烟按照 v = V ⋯ 0 v=V\cdots 0 v=V⋯0的逆序来循环, 是因为要保证第i次循环中的状态 f [ i ] [ v ] f[i][v] f[i][v]是由状态 f [ i − 1 ] [ v − c [ i ] ] f[i-1][v-c[i]] f[i−1][v−c[i]]递推而来, 换句话说, 这正是保证每件物品只选一次, 保证在考虑“选入第 i i i件物品”这件策略时, 依据的是一个绝无已经选入第i件物品的子结果 f [ i − 1 ] [ v − c [ i ] ] f[i-1][v-c[i]] f[i−1][v−c[i]].
-
然而, 现在完全背包问题的特点恰是每种物品可选无限件, 所以在考虑“加选一件第 i i i种物品”这种策略时, 却正需要一个可能已选入第i种物品的子结果 f [ i ] [ v − c [ i ] ] f[i][v-c[i]] f[i][v−c[i]], 所以就可以并且必须采用 v = 0 ⋯ V v=0\cdots V v=0⋯V的顺序循环. 这就是这个简单的程序为何成立的道理.
-
这个算法也可以以另外的思路得出. 例如, 基本思路中的状态转换方程可以等价的变形成这种形式: f [ i ] [ v ] = m a x { f [ i − 1 ] [ v ] , f [ i ] [ v − c [ i ] ] + w [ i ] } f[i][v]=max\{f[i-1][v], f[i][v-c[i]]+w[i]\} f[i][v]=max{f[i−1][v],f[i][v−c[i]]+w[i]} 将这个方程用一维数组实现, 得到了上面的伪代码.
最后抽象出处理一件完全背包类问题的过程伪代码, 如下所示
procedure CompeletePack(cost, weigth):
for v=cost..V:
f[v]=max{f[v], f[v-c[i]]+w[i]}
总结
完全背包问题也是一个相当基础的背包问题, 它有两个状态转移方程, 分别在“基本思路”以及“ O ( V × N ) O(V\times N) O(V×N)算法”的小节给出.