hurrican6

C++博客 首页 新随笔 联系 聚合 管理
  3 Posts :: 1 Stories :: 0 Comments :: 0 Trackbacks

2010年8月8日 #

P01: 01背包问题 
题目 
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。 

基本思路 
这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。 

用子问题定义状态:即f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}。 

这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。所以有必要将它详细解释一下:“将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”;如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为v-c[i]的背包中”,此时能获得的最大价值就是f [i-1][v-c[i]]再加上通过放入第i件物品获得的价值w[i]。 

注意f[i][v]有意义当且仅当存在一个前i件物品的子集,其费用总和为v。所以按照这个方程递推完毕后,最终的答案并不一定是f[N] [V],而是f[N][0..V]的最大值。如果将状态的定义中的“恰”字去掉,在转移方程中就要再加入一项f[i][v-1],这样就可以保证f[N] [V]就是最后的答案。至于为什么这样就可以,由你自己来体会了。 

优化空间复杂度 
以上方法的时间和空间复杂度均为O(N*V),其中时间复杂度基本已经不能再优化了,但空间复杂度却可以优化到O(V)。 

先考虑上面讲的基本思路如何实现,肯定是有一个主循环i=1..N,每次算出来二维数组f[i][0..V]的所有值。那么,如果只用一个数组f [0..V],能不能保证第i次循环结束后f[v]中表示的就是我们定义的状态f[i][v]呢?f[i][v]是由f[i-1][v]和f[i-1] [v-c[i]]两个子问题递推而来,能否保证在推f[i][v]时(也即在第i次主循环中推f[v]时)能够得到f[i-1][v]和f[i-1][v -c[i]]的值呢?事实上,这要求在每次主循环中我们以v=V..0的顺序推f[v],这样才能保证推f[v]时f[v-c[i]]保存的是状态f[i -1][v-c[i]]的值。伪代码如下: 

for i=1..N 
for v=V..0 
f[v]=max{f[v],f[v-c[i]]+w[i]}; 

其中的f[v]=max{f[v],f[v-c[i]]}一句恰就相当于我们的转移方程f[i][v]=max{f[i-1][v],f[i- 1][v-c[i]]},因为现在的f[v-c[i]]就相当于原来的f[i-1][v-c[i]]。如果将v的循环顺序从上面的逆序改成顺序的话,那么则成了f[i][v]由f[i][v-c[i]]推知,与本题意不符,但它却是另一个重要的背包问题P02最简捷的解决方案,故学习只用一维数组解01背包问题是十分必要的。 

总结 
01背包问题是最基本的背包问题,它包含了背包问题中设计状态、方程的最基本思想,另外,别的类型的背包问题往往也可以转换成01背包问题求解。故一定要仔细体会上面基本思路的得出方法,状态转移方程的意义,以及最后怎样优化的空间复杂度。 

P02: 完全背包问题 
题目 
有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。 

基本思路 
这个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……等很多种。如果仍然按照解01背包时的思路,令f[i][v]表示前i种物品恰放入一个容量为v的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程,像这样:f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k*c[i]<= v}。这跟01背包问题一样有O(N*V)个状态需要求解,但求解每个状态的时间则不是常数了,求解状态f[i][v]的时间是O(v/c[i]),总的复杂度是超过O(VN)的。 

将01背包问题的基本思路加以改进,得到了这样一个清晰的方法。这说明01背包问题的方程的确是很重要,可以推及其它类型的背包问题。但我们还是试图改进这个复杂度。 

一个简单有效的优化 
完全背包问题有一个很简单有效的优化,是这样的:若两件物品i、j满足c[i]<=c[j]且w[i]>=w[j],则将物品j去掉,不用考虑。这个优化的正确性显然:任何情况下都可将价值小费用高得j换成物美价廉的i,得到至少不会更差的方案。对于随机生成的数据,这个方法往往会大大减少物品的件数,从而加快速度。然而这个并不能改善最坏情况的复杂度,因为有可能特别设计的数据可以一件物品也去不掉。 

转化为01背包问题求解 
既然01背包问题是最基本的背包问题,那么我们可以考虑把完全背包问题转化为01背包问题来解。最简单的想法是,考虑到第i种物品最多选V/c [i]件,于是可以把第i种物品转化为V/c[i]件费用及价值均不变的物品,然后求解这个01背包问题。这样完全没有改进基本思路的时间复杂度,但这毕竟给了我们将完全背包问题转化为01背包问题的思路:将一种物品拆成多件物品。 

更高效的转化方法是:把第i种物品拆成费用为c[i]*2^k、价值为w[i]*2^k的若干件物品,其中k满足c[i]*2^k<V。这是二进制的思想,因为不管最优策略选几件第i种物品,总可以表示成若干个2^k件物品的和。这样把每种物品拆成O(log(V/c[i]))件物品,是一个很大的改进。 但我们有更优的O(VN)的算法。 * O(VN)的算法 这个算法使用一维数组,先看伪代码: <pre class"example"> for i=1..N for v=0..V f[v]=max{f[v],f[v-c[i]]+w[i]}; 



你会发现,这个伪代码与P01的伪代码只有v的循环次序不同而已。为什么这样一改就可行呢?首先想想为什么P01中要按照v=V..0的逆序来循环。这是因为要保证第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= 0..V的顺序循环。这就是这个简单的程序为何成立的道理。 

这个算法也可以以另外的思路得出。例如,基本思路中的状态转移方程可以等价地变形成这种形式:f[i][v]=max{f[i-1][v],f[i][v-c[i]]+w[i]},将这个方程用一维数组实现,便得到了上面的伪代码。 

总结 
完全背包问题也是一个相当基础的背包问题,它有两个状态转移方程,分别在“基本思路”以及“O(VN)的算法“的小节中给出。希望你能够对这两个状态转移方程都仔细地体会,不仅记住,也要弄明白它们是怎么得出来的,最好能够自己想一种得到这些方程的方法。事实上,对每一道动态规划题目都思考其方程的意义以及如何得来,是加深对动态规划的理解、提高动态规划功力的好方法。 

P03: 多重背包问题 
题目 
有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。 

基本算法 
这题目和完全背包问题很类似。基本的方程只需将完全背包问题的方程略微一改即可,因为对于第i种物品有n[i]+1种策略:取0件,取1件……取 n[i]件。令f[i][v]表示前i种物品恰放入一个容量为v的背包的最大权值,则:f[i][v]=max{f[i-1][v-k*c[i]]+ k*w[i]|0<=k<=n[i]}。复杂度是O(V*∑n[i])。 

转化为01背包问题 
另一种好想好写的基本方法是转化为01背包求解:把第i种物品换成n[i]件01背包中的物品,则得到了物品数为∑n[i]的01背包问题,直接求解,复杂度仍然是O(V*∑n[i])。 

但是我们期望将它转化为01背包问题之后能够像完全背包一样降低复杂度。仍然考虑二进制的思想,我们考虑把第i种物品换成若干件物品,使得原问题中第i种物品可取的每种策略——取0..n[i]件——均能等价于取若干件代换以后的物品。另外,取超过n[i]件的策略必不能出现。 

方法是:将第i种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来的费用和价值乘以这个系数。使这些系数分别为 1,2,4,...,2^(k-1),n[i]-2^k+1,且k是满足n[i]-2^k+1>0的最大整数。例如,如果n[i]为13,就将这种物品分成系数分别为1,2,4,6的四件物品。 

分成的这几件物品的系数和为n[i],表明不可能取多于n[i]件的第i种物品。另外这种方法也能保证对于0..n[i]间的每一个整数,均可以用若干个系数的和表示,这个证明可以分0..2^k-1和2^k..n[i]两段来分别讨论得出,并不难,希望你自己思考尝试一下。 

这样就将第i种物品分成了O(log n[i])种物品,将原问题转化为了复杂度为O(V*∑log n[i])的01背包问题,是很大的改进。 

O(VN)的算法 
多重背包问题同样有O(VN)的算法。这个算法基于基本算法的状态转移方程,但应用单调队列的方法使每个状态的值可以以均摊O(1)的时间求解。由于用单调队列优化的DP已超出了NOIP的范围,故本文不再展开讲解。我最初了解到这个方法是在楼天成的“男人八题”幻灯片上。 

小结 
这里我们看到了将一个算法的复杂度由O(V*∑n[i])改进到O(V*∑log n[i])的过程,还知道了存在应用超出NOIP范围的知识的O(VN)算法。希望你特别注意“拆分物品”的思想和方法,自己证明一下它的正确性,并用尽量简洁的程序来实现。 



P04: 混合三种背包问题 
问题 
如果将P01、P02、P03混合起来。也就是说,有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)。应该怎么求解呢? 

01背包与完全背包的混合 
考虑到在P01和P02中最后给出的伪代码只有一处不同,故如果只有两类物品:一类物品只能取一次,另一类物品可以取无限次,那么只需在对每个物品应用转移方程时,根据物品的类别选用顺序或逆序的循环即可,复杂度是O(VN)。伪代码如下: 

for i=1..N 
if 第i件物品是01背包 
for v=V..0 
f[v]=max{f[v],f[v-c[i]]+w[i]}; 
else if 第i件物品是完全背包 
for v=0..V 
f[v]=max{f[v],f[v-c[i]]+w[i]}; 

再加上多重背包 
如果再加上有的物品最多可以取有限次,那么原则上也可以给出O(VN)的解法:遇到多重背包类型的物品用单调队列解即可。但如果不考虑超过NOIP范围的算法的话,用P03中将每个这类物品分成O(log n[i])个01背包的物品的方法也已经很优了。 

小结 
有人说,困难的题目都是由简单的题目叠加而来的。这句话是否公理暂且存之不论,但它在本讲中已经得到了充分的体现。本来01背包、完全背包、多重背包都不是什么难题,但将它们简单地组合起来以后就得到了这样一道一定能吓倒不少人的题目。但只要基础扎实,领会三种基本背包问题的思想,就可以做到把困难的题目拆分成简单的题目来解决。 
P05: 二维费用的背包问题 
问题 
二维费用的背包问题是指:对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。设这两种代价分别为代价1和代价2,第i件物品所需的两种代价分别为a[i]和b[i]。两种代价可付出的最大值(两种背包容量)分别为V和U。物品的价值为w[i]。 

算法 
费用加了一维,只需状态也加一维即可。设f[i][v][u]表示前i件物品付出两种代价分别为v和u时可获得的最大价值。状态转移方程就是:f [i][v][u]=max{f[i-1][v][u],f[i-1][v-a[i]][u-b[i]]+w[i]}。如前述方法,可以只使用二维的数组:当每件物品只可以取一次时变量v和u采用顺序的循环,当物品有如完全背包问题时采用逆序的循环。当物品有如多重背包问题时拆分物品。 

物品总个数的限制 
有时,“二维费用”的条件是以这样一种隐含的方式给出的:最多只能取M件物品。这事实上相当于每件物品多了一种“件数”的费用,每个物品的件数费用均为1,可以付出的最大件数费用为M。换句话说,设f[v][m]表示付出费用v、最多选m件时可得到的最大价值,则根据物品的类型(01、完全、多重)用不同的方法循环更新,最后在f[0..V][0..M]范围内寻找答案。 

另外,如果要求“恰取M件物品”,则在f[0..V][M]范围内寻找答案。 

小结 
事实上,当发现由熟悉的动态规划题目变形得来的题目时,在原来的状态中加一纬以满足新的限制是一种比较通用的方法。希望你能从本讲中初步体会到这种方法。 

P06: 分组的背包问题 
问题 
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。 

算法 
这个问题变成了每组物品有若干种策略:是选择本组的某一件,还是一件都不选。也就是说设f[k][v]表示前k组物品花费费用v能取得的最大权值,则有f[k][v]=max{f[k-1][v],f[k-1][v-c[i]]+w[i]|物品i属于第k组}。 

使用一维数组的伪代码如下: 

for 所有的组k 
for 所有的i属于组k 
for v=V..0 
f[v]=max{f[v],f[v-c[i]]+w[i]} 

另外,显然可以对每组中的物品应用P02中“一个简单有效的优化”。 

小结 
分组的背包问题将彼此互斥的若干物品称为一个组,这建立了一个很好的模型。不少背包问题的变形都可以转化为分组的背包问题(例如P07),由分组的背包问题进一步可定义“泛化物品”的概念,十分有利于解题。 

P07: 有依赖的背包问题 
简化的问题 
这种背包问题的物品间存在某种“依赖”的关系。也就是说,i依赖于j,表示若选物品i,则必须选物品j。为了简化起见,我们先设没有某个物品既依赖于别的物品,又被别的物品所依赖;另外,没有某件物品同时依赖多件物品。 

算法 
这个问题由NOIP2006金明的预算方案一题扩展而来。遵从该题的提法,将不依赖于别的物品的物品称为“主件”,依赖于某主件的物品称为“附件”。由这个问题的简化条件可知所有的物品由若干主件和依赖于每个主件的一个附件集合组成。 

按照背包问题的一般思路,仅考虑一个主件和它的附件集合。可是,可用的策略非常多,包括:一个也不选,仅选择主件,选择主件后再选择一个附件,选择主件后再选择两个附件……无法用状态转移方程来表示如此多的策略。(事实上,设有n个附件,则策略有2^n+1个,为指数级。) 

考虑到所有这些策略都是互斥的(也就是说,你只能选择一种策略),所以一个主件和它的附件集合实际上对应于P06中的一个物品组,每个选择了主件又选择了若干个附件的策略对应于这个物品组中的一个物品,其费用和价值都是这个策略中的物品的值的和。但仅仅是这一步转化并不能给出一个好的算法,因为物品组中的物品还是像原问题的策略一样多。 

再考虑P06中的一句话: 可以对每组中的物品应用P02中“一个简单有效的优化”。这提示我们,对于一个物品组中的物品,所有费用相同的物品只留一个价值最大的,不影响结果。所以,我们可以对主件i的“附件集合”先进行一次01背包,得到费用依次为0..V-c[i]所有这些值时相应的最大价值f'[0..V-c[i]]。那么这个主件及它的附件集合相当于V-c[i]+1个物品的物品组,其中费用为c[i]+k的物品的价值为f'[k]+w[i]。也就是说原来指数级的策略中有很多策略都是冗余的,通过一次01背包后,将主件i转化为 V-c[i]+1个物品的物品组,就可以直接应用P06的算法解决问题了。 

更一般的问题 
更一般的问题是:依赖关系以图论中“森林”的形式给出(森林即多叉树的集合),也就是说,主件的附件仍然可以具有自己的附件集合,限制只是每个物品最多只依赖于一个物品(只有一个主件)且不出现循环依赖。 

解决这个问题仍然可以用将每个主件及其附件集合转化为物品组的方式。唯一不同的是,由于附件可能还有附件,就不能将每个附件都看作一个一般的01 背包中的物品了。若这个附件也有附件集合,则它必定要被先转化为物品组,然后用分组的背包问题解出主件及其附件集合所对应的附件组中各个费用的附件所对应的价值。 

事实上,这是一种树形DP,其特点是每个父节点都需要对它的各个儿子的属性进行一次DP以求得自己的相关属性。这已经触及到了“泛化物品”的思想。看完P08后,你会发现这个“依赖关系树”每一个子树都等价于一件泛化物品,求某节点为根的子树对应的泛化物品相当于求其所有儿子的对应的泛化物品之和。 

小结 
NOIP2006的那道背包问题我做得很失败,写了上百行的代码,却一分未得。后来我通过思考发现通过引入“物品组”和“依赖”的概念可以加深对这题的理解,还可以解决它的推广问题。用物品组的思想考虑那题中极其特殊的依赖关系:物品不能既作主件又作附件,每个主件最多有两个附件,可以发现一个主件和它的两个附件等价于一个由四个物品组成的物品组,这便揭示了问题的某种本质。 

我想说:失败不是什么丢人的事情,从失败中全无收获才是。 

P08: 泛化物品 
定义 
考虑这样一种物品,它并没有固定的费用和价值,而是它的价值随着你分配给它的费用而变化。这就是泛化物品的概念。 

更严格的定义之。在背包容量为V的背包问题中,泛化物品是一个定义域为0..V中的整数的函数h,当分配给它的费用为v时,能得到的价值就是h(v)。 

这个定义有一点点抽象,另一种理解是一个泛化物品就是一个数组h[0..V],给它费用v,可得到价值h[V]。 

一个费用为c价值为w的物品,如果它是01背包中的物品,那么把它看成泛化物品,它就是除了h(c)=w其它函数值都为0的一个函数。如果它是完全背包中的物品,那么它可以看成这样一个函数,仅当v被c整除时有h(v)=v/c*w,其它函数值均为0。如果它是多重背包中重复次数最多为n的物品,那么它对应的泛化物品的函数有h(v)=v/c*w仅当v被c整除且v/c<=n,其它情况函数值均为0。 

一个物品组可以看作一个泛化物品h。对于一个0..V中的v,若物品组中不存在费用为v的的物品,则h(v)=0,否则h(v)为所有费用为v的物品的最大价值。P07中每个主件及其附件集合等价于一个物品组,自然也可看作一个泛化物品。 

泛化物品的和 
如果面对两个泛化物品h和l,要用给定的费用从这两个泛化物品中得到最大的价值,怎么求呢?事实上,对于一个给定的费用v,只需枚举将这个费用如何分配给两个泛化物品就可以了。同样的,对于0..V的每一个整数v,可以求得费用v分配到h和l中的最大价值f(v)。也即f(v)=max{h(k) +l(v-k)|0<=k<=v}。可以看到,f也是一个由泛化物品h和l决定的定义域为0..V的函数,也就是说,f是一个由泛化物品h和 l决定的泛化物品。 

由此可以定义泛化物品的和:h、l都是泛化物品,若泛化物品f满足f(v)=max{h(k)+l(v-k)|0<=k<=v},则称f是h与l的和,即f=h+l。这个运算的时间复杂度是O(V^2)。 

泛化物品的定义表明:在一个背包问题中,若将两个泛化物品代以它们的和,不影响问题的答案。事实上,对于其中的物品都是泛化物品的背包问题,求它的答案的过程也就是求所有这些泛化物品之和的过程。设此和为s,则答案就是s[0..V]中的最大值。 

背包问题的泛化物品 
一个背包问题中,可能会给出很多条件,包括每种物品的费用、价值等属性,物品之间的分组、依赖等关系等。但肯定能将问题对应于某个泛化物品。也就是说,给定了所有条件以后,就可以对每个非负整数v求得:若背包容量为v,将物品装入背包可得到的最大价值是多少,这可以认为是定义在非负整数集上的一件泛化物品。这个泛化物品——或者说问题所对应的一个定义域为非负整数的函数——包含了关于问题本身的高度浓缩的信息。一般而言,求得这个泛化物品的一个子域(例如0..V)的值之后,就可以根据这个函数的取值得到背包问题的最终答案。 

综上所述,一般而言,求解背包问题,即求解这个问题所对应的一个函数,即该问题的泛化物品。而求解某个泛化物品的一种方法就是将它表示为若干泛化物品的和然后求之。 

小结 
本讲可以说都是我自己的原创思想。具体来说,是我在学习函数式编程的 Scheme 语言时,用函数编程的眼光审视各类背包问题得出的理论。这一讲真的很抽象,也许在“模型的抽象程度”这一方面已经超出了NOIP的要求,所以暂且看不懂也没关系。相信随着你的OI之路逐渐延伸,有一天你会理解的。 

我想说:“思考”是一个OIer最重要的品质。简单的问题,深入思考以后,也能发现更多。 

P09: 背包问题问法的变化 
以上涉及的各种背包问题都是要求在背包容量(费用)的限制下求可以取到的最大价值,但背包问题还有很多种灵活的问法,在这里值得提一下。但是我认为,只要深入理解了求背包问题最大价值的方法,即使问法变化了,也是不难想出算法的。 

例如,求解最多可以放多少件物品或者最多可以装满多少背包的空间。这都可以根据具体问题利用前面的方程求出所有状态的值(f数组)之后得到。 

还有,如果要求的是“总价值最小”“总件数最小”,只需简单的将上面的状态转移方程中的max改成min即可。 

下面说一些变化更大的问法。 

输出方案 
一般而言,背包问题是要求一个最优值,如果要求输出这个最优值的方案,可以参照一般动态规划问题输出方案的方法:记录下每个状态的最优值是由状态转移方程的哪一项推出来的,换句话说,记录下它是由哪一个策略推出来的。便可根据这条策略找到上一个状态,从上一个状态接着向前推即可。 

还是以01背包为例,方程为f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}。再用一个数组g[i] [v],设g[i][v]=0表示推出f[i][v]的值时是采用了方程的前一项(也即f[i][v]=f[i-1][v]),g[i][v]表示采用了方程的后一项。注意这两项分别表示了两种策略:未选第i个物品及选了第i个物品。那么输出方案的伪代码可以这样写(设最终状态为f[N][V]): 

i=N 
v=V 
while(i>0) 
if(g[i][v]==0) 
print "未选第i项物品" 
else if(g[i][v]==1) 
print "选了第i项物品" 
v=v-c[i] 

另外,采用方程的前一项或后一项也可以在输出方案的过程中根据f[i][v]的值实时地求出来,也即不须纪录g数组,将上述代码中的g[i] [v]==0改成f[i][v]==f[i-1][v],g[i][v]==1改成f[i][v]==f[i-1][v-c[i]]+w[i]也可。 

输出字典序最小的最优方案 
这里“字典序最小”的意思是1..N号物品的选择方案排列出来以后字典序最小。以输出01背包最小字典序的方案为例。 

一般而言,求一个字典序最小的最优方案,只需要在转移时注意策略。首先,子问题的定义要略改一些。我们注意到,如果存在一个选了物品1的最优方案,那么答案一定包含物品1,原问题转化为一个背包容量为v-c[1],物品为2..N的子问题。反之,如果答案不包含物品1,则转化成背包容量仍为V,物品为2..N的子问题。不管答案怎样,子问题的物品都是以i..N而非前所述的1..i的形式来定义的,所以状态的定义和转移方程都需要改一下。但也许更简易的方法是先把物品逆序排列一下,以下按物品已被逆序排列来叙述。 

在这种情况下,可以按照前面经典的状态转移方程来求值,只是输出方案的时候要注意:从N到1输入时,如果f[i][v]==f[i-v]及f[i][v]==f[i-1][f-c[i]]+w[i]同时成立,应该按照后者(即选择了物品i)来输出方案。 

求方案总数 
对于一个给定了背包容量、物品费用、物品间相互关系(分组、依赖等)的背包问题,除了再给定每个物品的价值后求可得到的最大价值外,还可以得到装满背包或将背包装至某一指定容量的方案总数。 

对于这类改变问法的问题,一般只需将状态转移方程中的max改成sum即可。例如若每件物品均是01背包中的物品,转移方程即为f[i][v]=sum{f[i-1][v],f[i-1][v-c[i]]+w[i]},初始条件f[0][0]=1。 

事实上,这样做可行的原因在于状态转移方程已经考察了所有可能的背包组成方案。 

最优方案的总数 
这里的最优方案是指物品总价值最大的方案。还是以01背包为例。 

结合求最大总价值和方案总数两个问题的思路,最优方案的总数可以这样求:f[i][v]意义同前述,g[i][v]表示这个子问题的最优方案的总数,则在求f[i][v]的同时求g[i][v]的伪代码如下: 

for i=1..N 
for v=0..V 
f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]} 
g[i][v]=0 
if(f[i][v]==f[i-1][v]) 
inc(g[i][v],g[i-1][v] 
if(f[i][v]==f[i-1][v-c[i]]+w[i]) 
inc(g[i][v],g[i-1][v-c[i]]) 

如果你是第一次看到这样的问题,请仔细体会上面的伪代码。 

小结 
显然,这里不可能穷尽背包类动态规划问题所有的问法。甚至还存在一类将背包类动态规划问题与其它领域(例如数论、图论)结合起来的问题,在这篇论背包问题的专文中也不会论及。但只要深刻领会前述所有类别的背包问题的思路和状态转移方程,遇到其它的变形问法,只要题目难度还属于NOIP,应该也不难想出算法。 

触类旁通、举一反三,应该也是一个OIer应有的品质吧。
posted @ 2010-08-08 16:33 comix 阅读(205) | 评论 (0)编辑 收藏

1.NOIP中,DP的出题方向

近年来,DP已成为NOIP中的“必考”项目,在06年的提高组题目中,甚至出现了两题DP(且该年分数线约为130分),DP的重要性可见一斑。
由于NOIP的难度所限,所出的DP基本上都是一些典型的模型加以稍许改编。

如下:

2003:加分二叉树:树型动态规划(区间类)。
2004:合唱队形:双向最长不降(升)序列。
2005:过河:需压缩空间的线性动态规划。
2006:能量项链:环状的合并类;金明的预算:需分类以取消后效性的01背包问题。
2007:矩阵取数:需高精度的区间类动态规划。

由此可见,NOIP在DP一块上的出题思路基本上是:不出刁钻的,罕见的动态规划类型,模式通常较易识别,但添加了部分新难点,以增加题目的区分度。这也就要求我们在复习DP算法时,集中注意力在一些典型的模型,以及了解其本质,才能拿下稍作变形的真题。


2. 一些经典的DP模型

    一道DP往往可以通过多种方式去做,所以以下分类并不完全准确,是相对而言的。大家不要死记某种类型,而应把握该类题型的本质和共性。

1? 不降(升)序列类&线性类

不降序列问题相信大家都做过。它的特点是线性递推,通常以某一结点为状态,转移是由前往后线性遍历。典型题目有导弹拦截和过河。由于大家都很熟悉了,加上今年来NOIP出了好几回,这里不做多说。

特别留意:
1. 不降(升)序列的O(nlogn)算法。(http://fqq11679.blog.hexun.com/21632261_d.html
2. 不降(升)序列中的方案个数。(http://blog.sina.com.cn/s/blog_4d801ace01000bj7.Html)
3. 对于大数据可能需根据实际进行压缩,主要通过转移方程决定。(*可能用到“维护”思想,http://www.rqnoj.cn/Problem_Show.asp?PID=386
4. 线性类题目可以隐藏得很深,构建模型时需多加留意能否用线性DP做。(http://live.cqfls.cn/ShowArticle.asp?ArticleID=709

1? 背包类

背包类问题也是大家常见的类型。它的特点是以前几样物品组成的集合为状态,决策也很明显——“取”与“不取”。背包类问题的变形十分多,这里强烈建议大家抽时间去看著名的《背包九讲》,里面写得相当详尽,掌握里面的内容后,背包类基本上不用担忧了。(http://www.concretevitamin.com.cn/informatics/Pack/Index.html

特别留意:
1.要保证无后效性,遇到设置后效性的题目可以分类处理(如金明的预算)。
2.如有多维,且维数太多,则考虑降低每次循环的次数或考虑其他思路。
3.在实现算法时,如果状态转移只在相邻两三个阶段间发生,则可用动态数组,可以减少空间需要。
4.背包类题目也可以出得很隐蔽,比如多米诺骨牌问题,重要的是抽取模型。
(见http://hi.baidu.com/rangemq/blog/item/9a6b5360c5e76145eaf8f842.html
5.需特别注意解数组的初始值设定,弄清楚解为0与无解的区别,常把初始值设为无穷小,也可都设为0,再在引用时判断是否是无解。

2? 路径类
    
路径类的典型例题有三角取数和花店橱窗布置。其特点是决策是“左走”,“右走”或“直走”。这类题目是十分典型的DP模型,但已有几年没出了,需留意。

特别留意:
1.注意边界的设置。
2.实现算法时可用动态数组,减少空间。
3.若题目设置“时间”概念,可能需要加维。

4)区间&合并类
   
区间类问题可以看做一个连续的大区间被分割成若干个有重叠的小区间,再从中选择最优解,而选择的依据就是转移方程。由于这种题长度为L的区间需要引用到长度不足L的区间的结果,所以常以长度作为阶段,起始位置为状态。合并类是在区间类基础上,以最佳合并为选择依据的一类题目。这类题目分别出在了06年和07年考题上——能量项链和矩阵取数,今年再出的可能性相对不大,但也不可轻视。

特别留意:
1.如遇环状模型,可从任意一结点断开,在后方补足,使之成为线性。
http://www.rqnoj.cn/Problem_Show.asp?PID=5
2.转移方程中,可能有需要预处理的内容,或用贪心算法。
    (http://www.vijos.cn/Problem_Show.asp?id=1242

5? 树型

树型动态规划,就是指建立在树这个数据结构上的动态规划。它的特点是以单个结点为状态,转移方程有该结点的孩子参与,计算过程自下往上进行。上一次出此类题目是5年前,说不准今年就会出。它的典型例题有选课和战略游戏。
http://www.vijos.cn/Problem_Show.asp?id=1180


特别留意:
1.掌握好树的读入方式,以及多叉树变二叉树的方式。
2.因为树的特殊结构,经常要分类讨论一个结点在做决策时的各种可能性,需要小心处理,考虑周全。(http://www.rqnoj.cn/Problem_Show.asp?PID=387

6)布尔型

这是一种相对少见的类型,其实还是属于背包类或线性类,只是它的最优值数组是布尔类型,所以我将其独立为一类。它的特点是最优解数组的类型为布尔类型,转移方程为逻辑运算,实际的问题最优解藏在状态之中。典型的题有砝码问题和取余问题
http://www.vijos.cn/Test_Problem_Show.asp?testid=1032&id=1455

特别留意:
1.使用前,要论证可以使用此类DP。
2.取余类问题中,状态设置应为[-(m-1)..(m-1)]或[0..(m-1)]。

7? 坐标类

这种类型也很少见,而且难度通常较大,不必过于关注。其特点是:在平面或立体内,以点坐标或相应矩形为状态。典型问题有棋盘分割和奶牛浴场。

特别留意:
1.此类题目往往根据几何关系或数学知识推断出转移方程。

8? 集合状态类

这种类型也不多见,但特点很突出。它往往具有多个状态维数,有时多达5,6个,而且决策与若干个状态组成一个整体,修改最值时一同更新。典型例题有购物和快餐问题。

特别留意:
1.确定使用此类DP后,大胆增设状态维数。
2.要找出切实可行的转移方程和算法实现方式。

9)字符串类

字符串类问题也不是一个专门的DP类型,这里专指一些利用到字符串特点的DP问题,如最长公共子序列和调整队形。它往往是两个字符串按一定的规则匹配,从而得到一系列最优解。常用的方法是以一个字符串中的一个字符去匹配另一个字符串的前面若干个字符构成的子串。(http://oi.tju.edu.cn/problem/view/1006.html

特别留意:
1.要确定最优解的状态的所有可能性。
2.多数时候此类问题还是属于线性类问题,需要结合线性类的特点设计算法。
3.可留意一下KMP算法。
4.可留意一下回文字一类题目。

3.NOIP可能会给模型增加的难度

1? 非线性数据类型

    在数据类型方面,NOIP最可能增添的难度是出环状和树状。
1)树状
对于树状,我们通常可以用树型DP解决。需留意的是,有些看似树型DP的题,其实可能是区间类DP,如03年的加分二叉树。另外,树的输入方式有很多种,我们必须熟悉如何恰当保存树的数据,否则即便会做DP也拿不了分。

2)环状
    对于环状,我们有两种处理方法将其破坏成链:
    1.确定某结点为起点,枚举该结点状态,每次枚举后DP求解,记录起点为该状态下得到的最优解。最后将各种可能的最优解筛选出问题的最优解。此类方法适用于线性DP和路径型DP。
    2.对于长度为n的环,任意选取一点为起点,由起点开始得到一条长度为n的链,将前面n-1长度的链复制并转移到链的末端,相当于将两条同样的链首尾相接。由此,环的任意一种单向遍历方式都可以在这个长度为2n-1的链中实现。此类方法适用于区间类DP。
    从本质上讲,这两种处理方式是完全一样的,既枚举起点的位置或状态,利用DP求解。需要留意的是,这种条件下的最优值的位置往往要特别考虑的。

2)结合其他算法

1)贪心
DP和贪心结合的例子很常见,有以下两种:
1. 通过贪心确定转移方程,也可以作为预处理部分。例题为邮局。
      (http://www.vijos.cn/Problem_Show.asp?id=1242
2.通过贪心确定最优解的上限或下限,从而减少循环量,在大规模数据时很有效。例题为快餐问题。

2)搜索
搜索和DP的结合,可以体现在两方面:
1. 记忆化搜索
所谓“记忆化搜索”就是在传统的搜索过程中,加入DP的“保留子问题最优解”思想,提高时间效率。从框架上讲,还是搜索算法,这里不作讨论。
2. 搜索中的局部进行DP求解
在搜索过程中,某个局部可能用到DP进行求解。例题为邮票面值设计。
http://www.vijos.cn/Problem_Show.asp?id=1179

3)字符串处理、高精度、排序、求质数等
这几样都是基础算法,可作为DP的预处理,这里不多做叙述。

3)提出其他任务

1)输出最优方案
这是很常见的一种题目要求,它不仅要求我们求出最优值的大小,还要确定与之对应的每个决策。通常有两个方法解决:
1. 增添记录每次决策的数组M。在每次做决策时,M中对应的状态也保留一个代表该决策的值。如01背包中,某个背包取或不取,M中对应的值为1或0。在得出最优解后,正向遍历M(构建队列)或逆向遍历M(构建栈),从而输出最优方案。
2. 一些题目的性质决定,在得出最优解后,可以通过贪心输出最优方案。如书的复制
     (http://www.rqnoj.cn/Problem_Show.asp?PID=349)
前一种方法具有普遍性,在时空允许下绝大部分DP题目适用。后一种题目在小范围内适用,但其编程复杂度和时空复杂度都相当理想。

2)求前k优解(方案)或第k优解(方案)
此问题可通过增设状态维数解决。在最优解数组中增加一维p表示同样状态下的第p优解,显然在DP过程中,只需保留每个状态的前k优解就足够。在状态转移时,算出新状态的前k个最优解,依次保存。需要注意的是,每一种可以得出新状态的前k个最优解的情况都要考虑到。另外,题目可能问前k个最优解(数字一样时当一种解,中间的策略不用考虑),也可能问前k个最优方案(不仅数字一样,而且策略也一样),做题时要加以区分。

3)求最优方案的个数
这个问题可以用1)和2)中的思想共同解决。增设数组C,表示一定状态下的最优方案的个数,在状态转移时,小心叠加即可。

4)同时问两类最优值
有时题目会要求两类最优值A和B。遇到这样的问题,要分清楚其依赖关系,比如题目实际要求输出A的最优解,以及在A最优的情况,B的最优解。如此一来我们依然以B为状态,A为实际的最优解进行DP求解。同时我们增设一个数组Q,在计算A的过程中,在保证A最优的情况下,保存最优的B;遇到更优的A时,也更新B的最优解。例题为找GF。(http://www.rqnoj.cn/Problem_Show.asp?PID=57

4? 增设小范围的后效性

如果你看到一道很熟悉的DP题,但又发现它在小范围内有后效性,那么也许在进行简单处理后,这题依然可以用DP求解。
这类题目类型有2:
1? 具后效性的状态与它控制的状态成主次关系
    典型的例子为金明的预算方案,若干状态被一个状态控制,那么可以把这几个状态连同控制它的状态作为一个类,将题目的所有状态分成若干个类进行处理。每个类中,如果要选择当中的次级状态,必须先选中它所依赖的状态。也可以理解成将题目各状态转化为树的形式进行DP,若要选择一个结点,必先选择它的父母,而兄弟间是没联系的。
2? 具后效性的状态与它控制的状态成并列关系

5? 多进程问题
    不能多次DP,必须增设状态维数。

6)大规模数据
   1)宏观上优化
状态划分能否降维,状态转移能否贪心或预处理,能否贪心求上限下限,能否加大空间以换取时间,能否压缩储存空间如使用动态数组,能否采用双向动态规划(不推荐)。
2)微观上优化
能否在每次循环时尽可能降低循环次数,能否通过对选项排序减少运算量,能否剔除不可能的选择(包括状态和答案)。
如果以上优化仍未能使DP算法达到可行程度,则要考虑题目是否用贪心策略或递推策略。

posted @ 2010-08-08 16:28 comix 阅读(798) | 评论 (0)编辑 收藏

【摘要】

    本文针对一类近期经常出现的有关最大(或最优)子矩形及相关变形问题,介绍了极大化思想在这类问题中的应用。分析了两个具有一定通用性的算法。并通过一些例题讲述了这些算法选择和使用时的一些技巧。

【关键字】 矩形,障碍点,极大子矩形

【正文】

一、   问题

最大子矩形问题:在一个给定的矩形网格中有一些障碍点,要找出网格内部不包含任何障碍点,且边界与坐标轴平行的最大子矩形。

这是近期经常出现的问题,例如冬令营2002的《奶牛浴场》,就属于最大子矩形问题。

Winter Camp2002,奶牛浴场

题意简述:(原题见论文附件)

John要在矩形牛场中建造一个大型浴场,但是这个大型浴场不能包含任何一个奶牛的产奶点,但产奶点可以出在浴场的边界上。John的牛场和规划的浴场都是矩形,浴场要完全位于牛场之内,并且浴场的轮廓要与牛场的轮廓平行或者重合。要求所求浴场的面积尽可能大。

参数约定:产奶点的个数S不超过5000,牛场的范围N×M不超过30000×30000

二、   定义和说明

首先明确一些概念。

1、定义有效子矩形为内部不包含任何障碍点且边界与坐标轴平行的子矩形。如图所示,第一个是有效子矩形(尽管边界上有障碍点),第二个不是有效子矩形(因为内部含有障碍点)。

2、极大有效子矩形:一个有效子矩形,如果不存在包含它且比它大的有效子矩形,就称这个有效子矩形为极大有效子矩形。(为了叙述方便,以下称为极大子矩形

3、定义最大有效子矩形为所有有效子矩形中最大的一个(或多个)。以下简称为最大子矩形。

三、   极大化思想

【定理1】在一个有障碍点的矩形中的最大子矩形一定是一个极大子矩形。

证明:如果最大子矩形A不是一个极大子矩形,那么根据极大子矩形的定义,存在一个包含A且比A更大的有效子矩形,这与“A是最大子矩形”矛盾,所以【定理1】成立。

四、   从问题的特征入手,得到两种常用的算法

定理1虽然很显然,但却是很重要的。根据定理1,我们可以得到这样一个解题思路:通过枚举所有的极大子矩形,就可以找到最大子矩形。下面根据这个思路来设计算法。

约定:为了叙述方便,设整个矩形的大小为n×m,其中障碍点个数为s

算法1

算法的思路是通过枚举所有的极大子矩形找出最大子矩形。根据这个思路可以发现,如果算法中有一次枚举的子矩形不是有效子矩形、或者不是极大子矩形,那么可以肯定这个算法做了“无用功”,这也就是需要优化的地方。怎样保证每次枚举的都是极大子矩形呢,我们先从极大子矩形的特征入手。

定理2】:一个极大子矩形的四条边一定都不能向外扩展。更进一步地说,一个有效子矩形是极大子矩形的充要条件是这个子矩形的每条边要么覆盖了一个障碍点,要么与整个矩形的边界重合。

定理2的正确性很显然,如果一个有效子矩形的某一条边既没有覆盖一个障碍点,又没有与整个矩形的边界重合,那么肯定存在一个包含它的有效子矩形。根据定理2,我们可以得到一个枚举极大子矩形的算法。为了处理方便,首先在障碍点的集合中加上整个矩形四角上的点。每次枚举子矩形的上下左右边界(枚举覆盖的障碍点),然后判断是否合法(内部是否有包含障碍点)。这样的算法时间复杂度为O(S5),显然太高了。考虑到极大子矩形不能包含障碍点,因此这样枚举4个边界显然会产生大量的无效子矩形。

考虑只枚举左右边界的情况。对于已经确定的左右边界,可以将所有处在这个边界内的点按从上到下排序,如图1中所示,每一格就代表一个有效子矩形。这样做时间复杂度为O(S3)。由于确保每次得到的矩形都是合法的,所以枚举量比前一种算法小了很多。但需要注意的是,这样做枚举的子矩形虽然是合法的,然而不一定是极大的。所以这个算法还有优化的余地。通过对这个算法不足之处的优化,我们可以得到一个高效的算法。

回顾上面的算法,我们不难发现,所枚举的矩形的上下边界都覆盖了障碍点或者与整个矩形的边界重合,问题就在于左右边界上。只有那些左右边界也覆盖了障碍点或者与整个矩形的边界重合的有效子矩形才是我们需要考察的极大子矩形,所以前面的算法做了不少“无用功”。怎么减少“无用功”呢,这里介绍一种算法(算法1),它可以用在不少此类题目上。

算法的思路是这样的,先枚举极大子矩形的左边界,然后从左到右依次扫描每一个障碍点,并不断修改可行的上下边界,从而枚举出所有以这个定点为左边界的极大子矩形。考虑如图2中的三个点,现在我们要确定所有以1号点为左边界的极大矩形。先将1号点右边的点按横坐标排序。然后按从左到右的顺序依次扫描1号点右边的点,同时记录下当前的可行的上下边界。

开始时令当前的上下边界分别为整个矩形的上下边界。然后开始扫描。第一次遇到2号点,以2号点作为右边界,结合当前的上下边界,就得到一个极大子矩形(如图3)。同时,由于所求矩形不能包含2号点,且2号点在1号点的下方,所以需要修改当前的下边界,即以2号点的纵坐标作为新的下边界。第二次遇到3号点,这时以3号点的横坐标作为右边界又可以得到一个满足性质1的矩形(如图4)。类似的,需要相应地修改上边界。以此类推,如 果这个点是在当前点(确定左边界的点)上方,则修改上边界;如果在下方,则修改下边界;如果处在同一行,则可中止搜索(因为后面的矩形面积都是0了)。由于已经在障碍点集合中增加了整个矩形右上角和右下角的两个点,所以不会遗漏右边界与整个矩形的右边重合的极大子矩形(如图5)。需要注意的是,如果扫描到的点不在当前的上下边界内,那么就不需要对这个点 进行处理。

这样做是否将所有的极大子矩形都枚举过了呢?可以发现,这样做只考虑到了左边界覆盖一个点的矩形,因此我们还需要枚举左边界与整个矩形的左边界重合的情况。这还可以分为两类情况。一种是左边界与整个举行的左边界重合,而右边界覆盖了一个障碍点的情况,对于这种情况,可以用类似的方法从右到左扫描每一个点作为右边界的情况。另一种是左右边界均与整个矩形的左右边界重合的情况,对于这类情况我们可以在预处理中完成:先将所有点按纵坐标排序,然后可以得到以相邻两个点的纵坐标为上下边界,左右边界与整个矩形的左右边界重合的矩形,显然这样的矩形也是极大子矩形,因此也需要被枚举到。

通过前面两步,可以枚举出所有的极大子矩形。算法1的时间复杂度是O(S2)。这样,可以解决大多数最大子矩形和相关问题了。

虽然以上的算法(算法1)看起来是比较高效的,但也有使用的局限性。可以发现,这个算法的复杂度只与障碍点的个数s有关。但对于某些问题,s最大有可能达到n×m,当s较大时,这个算法就未必能满足时间上的要求了。能否设计出一种依赖于nm的算法呢?这样在算法1不能奏效的时候我们还有别的选择。我们再重新从最基本的问题开始研究。

算法2

     首先,根据定理1:最大有效子矩形一定是一个极大子矩形。不过与前一种算法不同的是,我们不再要求每一次枚举的一定是极大子矩形而只要求所有的极大子矩形都被枚举到。看起来这种算法可能比前一种差,其实不然,因为前一种算法并不是完美的:虽然每次考察的都是极大子矩形,但它还是做了一定量的“无用功”。可以发现,当障碍点很密集的时候,前一种算法会做大量没用的比较工作。要解决这个问题,我们必须跳出前面的思路,重新考虑一个新的算法。注意到极大子矩形的个数不会超过矩形内单位方格的个数,因此我们有可能找出一种时间复杂度是O(N×M)的算法。

定义:

有效竖线:除了两个端点外,不覆盖任何障碍点的竖直线段。

悬线:上端点覆盖了一个障碍点或达到整个矩形上端的有效竖线。如图所示的三个有效竖线都是悬线。

    对于任何一个极大子矩形,它的上边界上要么有一个障碍点,要么和整个矩形的上边界重合。那么如果把一个极大子矩形按x坐标不同切割成多个(实际上是无数个)与y轴垂直的线段,则其中一定存在一条悬线。而且一条悬线通过尽可能地向左右移动恰好能得到一个子矩形(未必是极大子矩形,但只可能向下扩展)。通过以上的分析,我们可以得到一个重要的定理。

定理3】:如果将一个悬线向左右两个方向尽可能移动所得到的有效子矩形称为这个悬线所对应的子矩形,那么所有悬线所对应的有效子矩形的集合一定包含了所有极大子矩形的集合。

定理3中的“尽可能”移动指的是移动到一个障碍点或者矩形边界的位置。

根据【定理3】可以发现,通过枚举所有的悬线,就可以枚举出所有的极大子矩形。由于每个悬线都与它底部的那个点一一对应,所以悬线的个数=(n-1)×m(以矩形中除了顶部的点以外的每个点为底部,都可以得到一个悬线,且没有遗漏)。如果能做到对每个悬线的操作时间都为O(1),那么整个算法的复杂度就是O(NM)。这样,我们看到了解决问题的希望。

    现在的问题是,怎样在O(1)的时间内完成对每个悬线的操作。我们知道,每个极大子矩形都可以通过一个悬线左右平移得到。所以,对于每个确定了底部的悬线,我们需要知道有关于它的三个量:顶部、左右最多能移动到的位置。对于底部为(i,j)的悬线,设它的高为hight[i,j],左右最多能移动到的位置为left[i,j],right[i,j]。为了充分利用以前得到的信息,我们将这三个函数用递推的形式给出。

对于以点(i,j)为底部的悬线:

如果点(i1,j)为障碍点,那么,显然以(i,j)为底的悬线高度为1,而且左右均可以移动到整个矩形的左右边界,即

如果点(i1,j)不是障碍点,那么,以(i,j)为底的悬线就等于以(i-1,j)为底的悬线+点(i,j)到点(i-1,j)的线段。因此,height[i,j]=height[i-1,j]+1。比较麻烦的是左右边界,先考虑left[i,j]。如下图所示,(i,j)对应的悬线左右能移动的位置要在(i-1,j)的基础上变化。

left[i,j]=max

(i,j)当前点

某个障碍
left[i-1,j]
left[i,j]
(i-1,j)

right[i,j]
的求法类似。综合起来,可以得到这三个参数的递推式:

       

这样做充分利用了以前得到的信息,使每个悬线的处理时间复杂度为O(1)。对于以点(i,j)为底的悬线对应的子矩形,它的面积为(right[i,j]-left[i,j])*height[i,j]

这样最后问题的解就是:

Result

max

整个算法的时间复杂度为O(NM),空间复杂度是O(NM)

两个算法的对比:

以上说了两种具有一定通用性的处理算法,时间复杂度分别为O(S2)O(NM)。两种算法分别适用于不同的情况。从时间复杂度上来看,第一种算法对于障碍点稀疏的情况比较有效,第二种算法则与障碍点个数的多少没有直接的关系(当然,障碍点较少时可以通过对障碍点坐标的离散化来减小处理矩形的面积,不过这样比较麻烦,不如第一种算法好),适用于障碍点密集的情况。

五、   例题

将前面提出的两种算法运用于具体的问题。

1、    Winter Camp2002,奶牛浴场

分析

题目的数学模型就是给出一个矩形和矩形中的一些障碍点,要求出矩形内的最大有效子矩形。这正是我们前面所讨论的最大子矩形问题,因此前两种算法都适用于这个问题。

下面分析两种算法运用在本题上的优略:

对于第一种算法,不用加任何的修改就可以直接应用在这道题上,时间复杂度为O(S2)S为障碍点个数;空间复杂度为O(S)

对于第二种算法,需要先做一定的预处理。由于第二种算法复杂度与牛场的面积有关,而题目中牛场的面积很大(30000×30000),因此需要对数据进行离散化处理。离散化后矩形的大小降为S×S,所以时间复杂度为O(S2),空间复杂度为O(S)。说明:需要注意的是,为了保证算法能正确执行,在离散化的时候需要加上S个点,因此实际需要的时间和空间较大,而且编程较复杂。

从以上的分析来看,无论从时空效率还是编程复杂度的角度来看,这道题采用第一种算法都更优秀。

2、    OIBH模拟赛1,提高组,Candy

题意简述:(原题见论文附件)

一个被分为 n*m 个格子的糖果盒,第 i 行第 j 列位置的格子里面有 a [i,j] 颗糖。但糖果盒的一些格子被老鼠洗劫。现在需要尽快从这个糖果盒里面切割出一个矩形糖果盒,新的糖果盒不能有洞,并且希望保留在新糖果盒内的糖的总数尽量多。

参数约定:1 nm 1000

分析

首先需要注意的是:本题的模型是一个矩阵,而不是矩形。在矩阵的情况下,由于点的个数是有限的,所以又产生了一个新的问题:最大权值子矩阵

   定义:

   有效子矩阵为内部不包含任何障碍点的子矩形。与有效子矩形不同,有效子矩阵地边界上也不能包含障碍点。

有效子矩阵的权值(只有有效子矩形才有权值)为这个子矩阵包含的所有点的权值和。

最大权值有效子矩阵为所有有效子矩阵中权值最大的一个。以下简称为最大权值子矩阵

本题的数学模型就是正权值条件下的最大权值子矩阵问题。再一次利用极大化思想,因为矩阵中的权值都是正的,所以最大权值子矩阵一定是一个极大子矩阵。所以我们只需要枚举所有的极大子矩阵,就能从中找到最大权值子矩阵。同样,两种算法只需稍加修改就可以解决本题。下面分析两种算法应用在本题上的优略:

对于第一种算法,由于矩形中障碍点的个数是不确定的,而且最大有可能达到N×M,这样时间复杂度有可能达到O(N2M2),空间复杂度为O(NM)。此外,由于矩形与矩阵的不同,所以在处理上会有一些小麻烦。

对于第二种算法,稍加变换就可以直接使用,时间复杂度为O(NM),空间复杂度为ONM)。

可以看出,第一种算法并不适合这道题,因此最好还是采用第二种算法。

3、    Usaco Training, Section 1.5.4, Big Barn

题意简述(原题见论文附件)

    Farmer John想在他的正方形农场上建一个正方形谷仓。由于农场上有一些树,而且Farmer John又不想砍这些树,因此要找出最大的一个不包含任何树的一块正方形场地。每棵树都可以看成一个点。

参数约定:牛场为N×N的,树的棵数为TN≤1000,T≤10000

分析

    这题是矩形上的问题,但要求的是最大子正方形。首先,明确一些概念。

1、定义有效子正方形为内部不包含任何障碍点的子正方形

2、定义极大有效子正方形为不能再向外扩展的有效子正方形,一下简称极大子正方形

3、定义最大有效子正方形为所有有效子正方形中最大的一个(或多个),以下简称最大子正方形

本题的模型有一些特殊,要在一个含有一些障碍点的矩形中求最大子正方形。这与前两题的模型是否有相似之处呢?还是从最大子正方形的本质开始分析。

与前面的情况类似,利用极大化思想,我们可以得到一个定理:

定理4】:在一个有障碍点的矩形中的最大有效子正方形一定是一个极大有效子正方形。

     

             根据【定理4,我们只需要枚举出所有的极大子正方形,就可以从中找出最大子正方形。极大子正方形有什么特征呢?所谓极大,就是不能再向外扩展。如果是极大子矩形,那么不能再向外扩展的充要条件是四条边上都覆盖了障碍点(【定理2】)。类似的,我们可以知道,一个有效子正方形是极大子正方形的充要条件是它任何两条相邻的边上都覆盖了至少一个障碍点。根据这一点,可以得到一个重要的定理。

定理5】:每一个极大子正方形都至少被一个极大子矩形包含。且这个极大子正方形一定有两条不相邻的边与这个包含它的极大子矩形的边重合。

根据【定理5,我们只需要枚举所有的极大子矩形,并检查它所包含的极大子正方形(一个极大子矩形包含的极大子正方形都是一样大的)是否是最大的就可以了。这样,问题的实质和前面所说的最大子矩形问题是一样的,同样的,所采用的算法也是一样的。

因为算法1和算法2都枚举出了所有的极大子矩形,因此,算法1和算法2都可以用在本题上。具体的处理方法如下:对于每一个枚举出的极大子矩形,如图所示,如果它的边长为ab,那么它包含的极大子正方形的边长即为min(a,b)

考虑到NT的大小不同,所以不同的算法会有不同的效果。下面分析两种算法应用在本题上的优略。

对于第一种算法,时间复杂度为O(T2),对于第二种算法,时间复杂度为O(N2)。因为N<T,所以从时间复杂度的角度看,第二种算法要比第一种算法好。考虑到两个算法的空间复杂度都可以承受,所以选择第二种算法较好些。

以下是第一种和第二种算法编程实现后在USACO Training Program Gateway上的运行时间。可以看出,在数据较大时,算法2的效率比算法1高。

算法1

Test 1: 0.009375

Test 2: 0.009375

Test 3: 0.009375

Test 4: 0.009375

Test 5: 0.009375

Test 6: 0.009375

Test 7: 0.021875

Test 8:     0.025

Test 9: 0.084375

Test 10:    0.3875

Test 11:     0.525

Test 12:    0.5625

Test 13: 0.690625

Test 14:   0.71875

Test 15:      0.75

算法2

Test 1: 0.009375

Test 2: 0.009375

Test 3: 0.009375

Test 4: 0.009375

Test 5: 0.009375

Test 6:   0.00625

Test 7: 0.009375

Test 8: 0.009375

Test 9:    0.0125

Test 10: 0.021875

Test 11: 0.028125

Test 12:   0.03125

Test 13:   0.03125

Test 14:   0.03125

Test 15: 0.034375

以上,利用极大化思想和前面设计的两个算法,通过转换模型,解决了三个具有一定代表性的例题。解题的关键就是如何利用极大化思想进行模型转换和如何选择算法。

  

五、小结

   设计算法要从问题的基本特征入手,找出解题的突破口。本文介绍了两种适用于大部分最大子矩形问题及相关变型问题的算法,它们设计的突破口就是利用了极大化思想,找到了枚举极大子矩形这种方法。

在效率上,两种算法对于不同的情况各有千秋。一个是针对障碍点来设计的,因此复杂度与障碍点有关;另一个是针对整个矩形来设计的,因此复杂度与矩形的面积有关。虽然两个算法看起来有着巨大的差别,但他们的本质是相通的,都是利用极大化思想,从枚举所有的极大有效子矩形入手,找出解决问题的方法。

        

需要注意的是,在解决实际问题是仅靠套用一些现有算法是不够的,还需要对问题进行全面、透彻的分析,找出解题的突破口。

此外,如果采用极大化思想,前面提到的两种算法的复杂度已经不能再降低了,因为极大有效子矩形的个数就是O(NM)O(S2)的。如果采用其他算法,理论上是有可能进一步提高算法效率,降低复杂度的。

posted @ 2010-08-08 12:30 comix 阅读(451) | 评论 (0)编辑 收藏

仅列出标题