前言
引文出自崔添翼背包9讲。
1. 题目对比
- 问题1:有 N 件物品和一个容量为 V 的背包。放入第 i 件物品耗费的费用是 Ci,得到的价值是 Wi。求解将哪些物品装入背包可使价值总和最大。
- 问题2:有N种物品和一个容量为V的背包,每种物品都有无限件可用。放入第i种物品的费用是Ci,价值是Wi。求解:将哪些物品装入背包,可使这些物品的耗费的费用总和不超过背包容量,且价值总和最大。
- 问题3:有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci,价值是Wi。求解将哪些物品装入背包可使这些物品的耗费的空间总和不超过背包容量,且价值总和最大。
其中问题2等价于每件有 V / C i V/Ci V/Ci的件数可用,现在将问题3变种, M i M_{i} Mi上做文章,变成:
如果将前面1、 2、 3中的三种背包问题混合起来。也就是说,有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)。应该怎么求解呢?
崔在背包九讲这么总结,笔者觉得可以更加准确的定义问题3的变种,就是 M i M_{i} Mi的取值可能是1,也可能 V / C i V/Ci V/Ci,还可能是 M i < V / C i M_{i}<V/Ci Mi<V/Ci件。思考:本质上看待其实就是 M i M_{i} Mi件给定的限制不同。
2. 一般的做法
for n=1 to N
for v=0 to V
for k=0 to logMi
if( v-2^kCi >=0 )//前面笔者没加这个,虽然说伪代码不用这么细节
F[i,v]=max{F[i-1,v-2^kCi]+2^kWi,F[i,v]}//使用max可以简化代码
该解法就是多重背包的解法,但是**能解决混合背包,我们将每种物品的限制通过 M i M_{i} Mi表现出来就可以了。**该算法的时间复杂度前面分析过为 O ( V ∑ l o g M i ) O(V\sum logMi) O(V∑logMi)。
3. 崔添翼的做法
for i 1 to N
if 第i件物品属于01背包
ZeroOnePack(F,Ci,Wi)
else if 第i件物品属于完全背包
CompletePack(F,Ci,Wi)
else if 第i件物品属于多重背包
MultiplePack(F,Ci,Wi,Ni)
如果仔细思考的话,觉得崔似乎多次一举。**但是该方式时间复杂度上确实优于上面的一般做法。**该做法的最优时间复杂度为 O ( N V ) O(NV) O(NV),最坏时间复杂度为 O ( V ∑ l o g M i ) O(V\sum logMi) O(V∑logMi)。平均时间复杂度不好分析,根据输入数据而定。
4. 崔添翼本意
在最初写出这三个过程的时候,可能完全没有想到它们会在这里混合应用。我想这体现了编程中抽象的威力。如果你一直就是以这种“抽象出过程”的方式写每一类背包问题的,也非常清楚它们的实现中细微的不同,那么在遇到混合三种背包问题的题目时,一定能很快想到上面简洁的解法,对吗?
刚开始我也奇怪,为什么第四讲会讲这个。问题抽象化是一种能力,能将新问题转化成若干旧问题。以后也要多学习将问题抽象,这样我们就有无数的类似于STL库方便的工具,让我们无所畏惧新问题。
最后搜索了一些ACMer的事情,有时候算法的常数优化都是比赛所看重的。何况上面的优化远远大于常数级了。(遗憾自己从来不是计算机专业,否则一定也是一名ACMer,拿不拿奖就不知道了。)