1、P01: 01 背包问题题目:有 N 件物品和一个容量为 V 的背包。第 i 件物品的费用是 ci,价值是 wi。求解将哪些物品装入背包可使价值总和最大。基本思路这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。用子问题定义状态:即 fiv表示前 i 件物品恰放入一个容量为 v 的背包可以获得的最大价值。则其状态转移方程便是:fiv=maxfi-1v,fi-1v-ci+wi这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。所以有必要将它详细解释一下:“将前 i 件物品放入容量为 v 的背包中”这个子问题,若只考虑第 i 件物品的策略(放或不放) ,那么就可以
2、转化为一个只牵扯前 i-1 件物品的问题。如果不放第 i 件物品,那么问题就转化为“前 i-1 件物品放入容量为 v 的背包中”,价值为 fi-1v;如果放第 i 件物品,那么问题就转化为“前 i-1 件物品放入剩下的容量为 v-ci的背包中”,此时能获得的最大价值就是 fi-1v-ci再加上通过放入第 i 件物品获得的价值 wi。优化空间复杂度以上方法的时间和空间复杂度均为 O(N*V),其中时间复杂度基本已经不能再优化了,但空间复杂度却可以优化到 O(V)。先考虑上面讲的基本思路如何实现,肯定是有一个主循环 i=1.N,每次算出来二维数组 fi0.V的所有值。那么,如果只用一个数组 f0.
3、V,能不能保证第 i 次循环结束后 fv中表示的就是我们定义的状态 fiv呢?fiv是由 fi-1v和 fi-1v-ci两个子问题递推而来,能否保证在推 fiv时(也即在第 i 次主循环中推 fv时)能够得到 fi-1v和 fi-1v-ci的值呢?事实上,这要求在每次主循环中我们以 v=V.0 的顺序推 fv,这样才能保证推 fv时 fv-ci保存的是状态 fi-1v-ci的值。伪代码如下:for i=1.Nfor v=V.0fv=maxfv,fv-ci+wi;其中的 fv=maxfv,fv-ci一句恰就相当于我们的转移方程 fiv=maxfi-1v,fi-1v-ci,因为现在的 fv-ci就
4、相当于原来的 fi-1v-ci。如果将 v 的循环顺序从上面的逆序改成顺序的话,那么则成了 fiv由 fiv-ci推知,与本题意不符,但它却是另一个重要的背包问题 P02 最简捷的解决方案,故学习只用一维数组解 01 背包问题是十分必要的。事实上,使用一维数组解 01 背包的程序在后面会被多次用到,所以这里抽象出一个处理一件 01 背包中的物品过程,以后的代码中直接调用不加说明。过程 ZeroOnePack,表示处理一件 01 背包中的物品,两个参数 cost、weight 分别表明这件物品的费用和价值。procedure ZeroOnePack(cost,weight)for v=V.cos
5、tfv=maxfv,fv-cost+weight注意这个过程里的处理与前面给出的伪代码有所不同。前面的示例程序写成 v=V.0 是为了在程序中体现每个状态都按照方程求解了,避免不必要的思维复杂度。而这里既然已经抽象成看作黑箱的过程了,就可以加入优化。费用为 cost 的物品不会影响状态 f0.cost-1,这是显然的。有了这个过程以后,01 背包问题的伪代码就可以这样写:for i=1.NZeroOnePack(ci,wi);初始化的细节问题我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。一种区别这两种
6、问法的实现方法是在初始化的时候有所不同。如果是第一种问法,要求恰好装满背包,那么在初始化时除了 f0为 0 其它 f1.V均设为- ,这样就可以保证最终得到的 fN是一种恰好装满背包的最优解。如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将 f0.V全部设为 0。为什么呢?可以这样理解:初始化的 f 数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为 0 的背包可能被价值为 0 的 nothing“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是-了。如果背包并非必须被装满,那么任何容量的背包都有一个合
7、法解“什么都不装” ,这个解的价值为 0,所以初始时状态的值也就全部为 0 了。这个小技巧完全可以推广到其它类型的背包问题,后面也就不再对进行状态转移之前的初始化进行讲解。小结:01 背包问题是最基本的背包问题,它包含了背包问题中设计状态、方程的最基本思想,另外,别的类型的背包问题往往也可以转换成 01 背包问题求解。故一定要仔细体会上面基本思路的得出方法,状态转移方程的意义,以及最后怎样优化的空间复杂度。P02: 完全背包问题题目:有 N 种物品和一个容量为 V 的背包,每种物品都有无限件可用。第 i 种物品的费用是 ci,价值是 wi。求解将哪些物品装入背包可使这些物品的费用总和不超过背包
8、容量,且价值总和最大。基本思路这个问题非常类似于 01 背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取 0 件、取 1 件、取 2 件等很多种。如果仍然按照解 01 背包时的思路,令 fiv表示前 i 种物品恰放入一个容量为 v 的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程,像这样:fiv=maxfi-1v-k*ci+k*wi|0=wj,则将物品 j去掉,不用考虑。这个优化的正确性显然:任何情况下都可将价值小费用高得 j 换成物美价廉的 i,得到至少不会更差的方案。对于随机生成的数据,这个方法往往会大大减少物品的
9、件数,从而加快速度。然而这个并不能改善最坏情况的复杂度,因为有可能特别设计的数据可以一件物品也去不掉。这个优化可以简单的 O(N2)地实现,一般都可以承受。另外,针对背包问题而言,比较不错的一种方法是:首先将费用大于 V 的物品去掉,然后使用类似计数排序的做法,计算出费用相同的物品中价值最高的是哪个,可以 O(V+N)地完成这个优化。这个不太重要的过程就不给出伪代码了,希望你能独立思考写出伪代码或程序。转化为 01 背包问题求解既然 01 背包问题是最基本的背包问题,那么我们可以考虑把完全背包问题转化为 01 背包问题来解。最简单的想法是,考虑到第 i 种物品最多选 V/ci件,于是可以把第
10、i 种物品转化为 V/ci件费用及价值均不变的物品,然后求解这个 01 背包问题。这样完全没有改进基本思路的时间复杂度,但这毕竟给了我们将完全背包问题转化为 01 背包问题的思路:将一种物品拆成多件物品。更高效的转化方法是:把第 i 种物品拆成费用为 ci*2k、价值为 wi*2k 的若干件物品,其中 k 满足 ci*2k0 的最大整数。例如,如果 ni为 13,就将这种物品分成系数分别为 1,2,4,6 的四件物品。分成的这几件物品的系数和为 ni,表明不可能取多于 ni件的第 i 种物品。另外这种方法也能保证对于 0.ni间的每一个整数,均可以用若干个系数的和表示,这个证明可以分 0.2k
11、-1 和 2k.ni两段来分别讨论得出,并不难,希望你自己思考尝试一下。这样就将第 i 种物品分成了 O(log ni)种物品,将原问题转化为了复杂度为 O(V*log ni)的 01 背包问题,是很大的改进。下面给出 O(log amount)时间处理一件多重背包中物品的过程,其中 amount 表示物品的数量:procedure MultiplePack(cost,weight,amount)if cost*amount=VCompletePack(cost,weight)returninteger k=1while k0)if(giv=0)print “未选第 i 项物品“else if
12、(giv=1)print “选了第 i 项物品“v=v-ci另外,采用方程的前一项或后一项也可以在输出方案的过程中根据 fiv的值实时地求出来,也即不须纪录 g数组,将上述代码中的 giv=0 改成 fiv=fi-1v,giv=1 改成 fiv=fi-1v-ci+wi也可。输出字典序最小的最优方案这里“字典序最小” 的意思是 1.N 号物品的选择方案排列出来以后字典序最小。以输出 01 背包最小字典序的方案为例。一般而言,求一个字典序最小的最优方案,只需要在转移时注意策略。首先,子问题的定义要略改一些。我们注意到,如果存在一个选了物品 1 的最优方案,那么答案一定包含物品 1,原问题转化为一个
13、背包容量为 v-c1,物品为 2.N 的子问题。反之,如果答案不包含物品 1,则转化成背包容量仍为 V,物品为 2.N 的子问题。不管答案怎样,子问题的物品都是以 i.N 而非前所述的 1.i 的形式来定义的,所以状态的定义和转移方程都需要改一下。但也许更简易的方法是先把物品逆序排列一下,以下按物品已被逆序排列来叙述。在这种情况下,可以按照前面经典的状态转移方程来求值,只是输出方案的时候要注意:从 N 到 1 输入时,如果 fiv=fi-v及 fiv=fi-1f-ci+wi同时成立,应该按照后者(即选择了物品 i)来输出方案。求方案总数对于一个给定了背包容量、物品费用、物品间相互关系(分组、依
14、赖等)的背包问题,除了再给定每个物品的价值后求可得到的最大价值外,还可以得到装满背包或将背包装至某一指定容量的方案总数。对于这类改变问法的问题,一般只需将状态转移方程中的 max 改成 sum 即可。例如若每件物品均是 01 背包中的物品,转移方程即为 fiv=sumfi-1v,fi-1v-ci+wi,初始条件 f00=1。事实上,这样做可行的原因在于状态转移方程已经考察了所有可能的背包组成方案。最优方案的总数这里的最优方案是指物品总价值最大的方案。还是以 01 背包为例。结合求最大总价值和方案总数两个问题的思路,最优方案的总数可以这样求:fiv意义同前述,giv表示这个子问题的最优方案的总数
15、,则在求 fiv的同时求 giv的伪代码如下:for i=1.Nfor v=0.Vfiv=maxfi-1v,fi-1v-ci+wigiv=0if(fiv=fi-1v)inc(giv,gi-1vif(fiv=fi-1v-ci+wi)inc(giv,gi-1v-ci)如果你是第一次看到这样的问题,请仔细体会上面的伪代码。小结显然,这里不可能穷尽背包类动态规划问题所有的问法。甚至还存在一类将背包类动态规划问题与其它领域(例如数论、图论)结合起来的问题,在这篇论背包问题的专文中也不会论及。但只要深刻领会前述所有类别的背包问题的思路和状态转移方程,遇到其它的变形问法,只要题目难度还属于 NOIP,应该也
16、不难想出算法。触类旁通、举一反三,应该也是一个 OIer 应有的品质吧。附:USACO 中的背包问题USACO 是 USA Computing Olympiad 的简称,它组织了很多面向全球的计算机竞赛活动。USACO Trainng 是一个很适合初学者的题库,我认为它的特色是题目质量高,循序渐进,还配有不错的课文和题目分析。其中关于背包问题的那篇课文 (TEXT Knapsack Problems) 也值得一看。另外, USACO Contest 是 USACO 常年组织的面向全球的竞赛系列,在此也推荐 NOIP 选手参加。我整理了 USACO Training 中涉及背包问题的题目,应该可
17、以作为不错的习题。其中标加号的是我比较推荐的,标叹号的是我认为对 NOIP 选手比较有挑战性的。题目列表Inflate (+) (基本 01 背包) Stamps (+)(!) (对初学者有一定挑战性) Money Nuggets Subsets Rockers (+) (另一类有趣的 “二维”背包问题) Milk4 (!) (很怪的背包问题问法,较难用纯 DP 求解) 题目简解以下文字来自我所撰的USACO 心得一文,该文的完整版本,包括我的程序,可在 DD 的 USACO 征程中找到。Inflate 是加权 01 背包问题,也就是说:每种物品只有一件,只可以选择放或者不放;而且每种物品有对
18、应的权值,目标是使总权值最大或最小。它最朴素的状态转移方程是:fki = maxfk-1i , fk-1i-vk+wk。fki表示前 k 件物品花费代价 i 可以得到的最大权值。vk和 wk分别是第 k 件物品的花费和权值。可以看到, fk的求解过程就是使用第 k 件物品对 fk-1进行更新的过程。那么事实上就不用使用二维数组,只需要定义 fi,然后对于每件物品 k,顺序地检查 fi与 fi-vk+wk的大小,如果后者更大,就对前者进行更新。这是背包问题中典型的优化方法。题目 stamps 中,每种物品的使用量没有直接限制,但使用物品的总量有限制。求第一个不能用这有限个物品组成的背包的大小。
19、(可以这样等价地认为)设 fki 表示前 k 件物品组成大小为 i 的背包, 最少需要物品的数量。则 fki= minfk-1i,fk-1i-j*sk+j,其中 j 是选择使用第 k 件物品的数目,这个方程运用时可以用和上面一样的方法处理成一维的。求解时先设置一个粗糙的循环上限,即最大的物品乘最多物品数。Money 是多重背包问题。也就是每个物品可以使用无限多次。要求解的是构成一种背包的不同方案总数。基本上就是把一般的多重背包的方程中的 min 改成 sum 就行了。Nuggets 的模型也是多重背包。要求求解所给的物品不能恰好放入的背包大小的最大值(可能不存在) 。只需要根据“若 i、j 互
20、质,则关于 x、y 的不定方程 i*x+y*j=n 必有正整数解,其中 ni*j”这一定理得出一个循环的上限。 Subsets 子集和问题相当于物品大小是前 N 个自然数时求大小为 N*(N+1)/4 的 01 背包的方案数。Rockers 可以利用求解背包问题的思想设计解法。我的状态转移方程如下: fijt=maxfijt-1 , fi-1jt , fi-1jt-timei+1 , fi-1j-1T+(t=timei)。其中 fijt表示前 i 首歌用 j 张完整的盘和一张录了 t 分钟的盘可以放入的最多歌数,T 是一张光盘的最大容量,t=timei是一个 bool 值转换成 int 取值为
21、 0 或 1。但我后来发现我当时设计的状态和方程效率有点低,如果换成这样:fij=(a,b)表示前 i 首歌中选了 j 首需要用到 a 张完整的光盘以及一张录了 b 分钟的光盘,会将时空复杂度都大大降低。这种将状态的值设为二维的方法值得注意。Milk4 是这些类背包问题中难度最大的一道了。很多人无法做到将它用纯 DP 方法求解,而是用迭代加深搜索枚举使用的桶,将其转换成多重背包问题再 DP。由于 USACO 的数据弱,迭代加深的深度很小,这样也可以AC,但我们还是可以用纯 DP 方法将它完美解决的。设 fk为称量出 k 单位牛奶需要的最少的桶数。那么可以用类似多重背包的方法对 f 数组反复更新以求得最小值。然而困难在于如何输出字典序最小的方案。我们可以对每个 i 记录 pre_fi和 pre_vi。表示得到 i 单位牛奶的过程是用 pre_fi单位牛奶加上若干个编号为pre_vi的桶的牛奶。这样就可以一步步求得得到 i 单位牛奶的完整方案。为了使方案的字典序最小,我们在每次找到一个耗费桶数相同的方案时对已储存的方案和新方案进行比较再决定是否更新方案。为了使这种比较快捷,在使用各种大小的桶对 f 数组进行更新时先大后小地进行。USACO 的官方题解正是这一思路。如果认为以上文字比较难理解可以阅读官方程序或我的程序。