因此可以看出原图事实上是一个偏序集的图表示。
链:
链是E的一个子集C,在偏序关系≤下,它的每一对元素都是可比的,即C是E的一个全序子集。
反链(或称杂置):
顾名思义,它和链的定义恰恰相反。
反链是E的一个子集A,在偏序关系≤下,它的每一对元素都是不可比的。
链和反链的大小是指集合中元素的个数。
**构筑原问题的偏序集模型
有了上文有关偏序集的概念,不难搭建出原问题的偏序集模型:
令原图表示的偏序集为(X,≤),而新构造的偏序集为(E,≤)。
则集合E满足
,即E中的元素全部是图中的有向边。
令a、b为E中的两个元素,设
。
当且仅当
时,有a≤b,即存在一条从有向边a到有向边b的路径。
当且仅当va=ub时,有a≤cb。
原问题可以重新用偏序集语言表述为:
将偏序集(E,≤)划分成最少的链,使得这些链的并集包含所有E中的元素。
直接计算链的个数似乎并不容易,好在有Dilworth定理揭示了链与反链的关系,从而使得问题的目标进一步转化。
定理2.1:
Dilworth定理令(E,≤)是一个有限偏序集,并令m是E中最大反链的大小,M是将E划分成最少的链的个数。
在E中,有m=M。
证明过程请参见附录。
根据Dilworth定理,问题转化成求E中最长的反链的大小。
也就是要求在原图中选尽量多的边,同时保证选出的边是互不可达的(即在(E,≤)中不可比)。
如何求解最长的反链呢?
事实上,这和原题给出的平面图有很大关系,接下来,返回到原图上继续讨论。
**从偏序集模型回归到原题
由于原题给出的图是平面图,而且图中顶点也是从左到右给出的,那么对于反链中的所有边都能按照从左到右的顺序排列好。
如果用一条线将最长反链所对应的边从左到右连起来(如图2-2所示),那么这条线不会与平面图中的其它边相交。
更加准确地说:
定理2.2:
将最长反链所对应的边从左到右排列好,相邻的两条边一定是在同一个域(闭曲面)中。
所谓的域,是由从一个点到另一个点(一个是极高点,一个是极低点)的两条不同路径(两条路径没有公共边)围成的一个曲面,在这个曲面里没有其他的点和边(如图2-3所示),记作F。
在围成域F的两条路径中,左边的那条路径定义为F的左边界,右边的那条路径定义为F的右边界。
关于定理2.2的简略证明请参见附录。
受定理2.2的启发,我们可以用递推的方法求得图中最长反链的长度。
设f(x)表示在边x左边的平面区域中以x结尾的最长反链的长度。
设x在某个域F的右边界上,有
。
因为根据定理2.2,若x在某个最长反链中,那么反链中和x相邻且在x左边的边,只有可能在域F的左边界上。
得到这个递推式后,只需要按照从左到右从上到下把每一个域求出进行递推即可。
**相应的算法设计
可以用DFS深度优先遍历实现平面图中域的寻找。
DFS中需要记录两个信息:
结点的颜色和扩展它的父结点。
每个结点的颜色用C[u]来记录。
C[u]有三种状态:
白色表示结点u尚未被遍历,一开始所有结点的颜色都是白色;灰色表示结点u已经被遍历,但是它尚未检查完毕,也就是说它还有后继结点没有扩展;黑色表示结点u不但被遍历且被检查完毕。
扩展它的父结点用pre记录。
算法的大致框架如下:
DFS(结点u)
begin
C[u]灰色
whilev是u后继结点do(按照从左到右的顺序扩展u的后继结点v)
ifC[v]是白色
thenbegin
pre[v]u
DFS(v)
endelsebegin
vlv
vhv
whileC[vh]是黑色do
vhpre[vh](是黑色,说明是域的边界上的结点;灰色就是极高点)
递推求出右边界的f(x)(pre回溯的边是左边界,
是右边界)
pre[v]u
end
C[u]黑色
end
计算上述算法的复杂度:
a.对每一个点进行DFS遍历,复杂度为O(|E|);b.回溯寻找每个域边界上的边,并且进行递推求解。
由于是平面图,每条边最多属于两个不同域的边界,因此这一步的复杂度为O(|E|+|F|)。
因为原题给出的图是平面图,根据欧拉定理,边数|E|和域数(或者说面数)|F|都是O(n)级别的。
因此总的时间复杂度为O(n)。
**小结
方法一利用网络流模型直接的体现了原题的网络(有向无环)特性,解法具有一般性,但是没有充分的体现出原题的平面图性质。
而方法二很好的利用偏序集模型实现了问题目标的转变,从原来的网络流问题回归到问题本身的平面图上,完整的揭示了问题的本质。
正是由于回归到问题的本质,后面才能用DFS充分挖掘平面图的性质,得到最优复杂度的算法。
从上述两种方法的比较可以看出,两种不同的图论模型导致了两种算法在时间复杂度上的较大差异,可见选择模型的重要性。
正所谓“磨刀不误砍柴功”,在设计算法之前,选择一个正确的图论模型往往能够起到事半功倍的效果,不仅能降低算法设计的难度,还使设计出的算法简单高效。
在为实际问题选择合适的图论模型的时候,不能仅仅局限于问题表面所表现的某些性质,只根据自有的经验去解题,否则会落入俗套,得不到效率最高的算法。
新题新作,应该创新思路,深入分析问题的各种性质,将这些性质结合在一起,从而寻找到最能体现问题本质的图论模型。
很多时候,最终算法并不是基于所选图论模型来设计的,就如方法二,偏序集并没有出现在DFS中,但一旦想到偏序集,问题可以说就解决了一半。
图论模型更多的是思考的一种过渡,使思路变得清晰,就像一座灯塔,指引你到成功的彼岸。
三、充分挖掘和利用模型的性质
上一节讲述了选择合适模型的重要性和搭建图论模型的方法。
建立模型仿佛是为算法设计搭建一个平台,接下来的工作是在这个平台上充分挖掘和利用原题的性质,设计一个解决问题的好算法。
如何挖掘和利用模型的性质呢?
下面同样用一个具体实例来说明。
例二:
爱丽丝和鲍勃(ACMCentralEuropeanProgrammingContest2001ProblemA:
AliceandBob)
题目描述:
爱丽丝和鲍勃在玩一个游戏。
爱丽丝画了一个有n(n≤10000)个顶点的凸多边形,多边形上的顶点从1到n按任意顺序标号。
然后她在多边形中画了几个不相交的对角线(公共点为顶点不算相交)。
她把每条边和对角线的端点标号告诉了鲍勃,但没有告诉他哪条是边,哪条是对角线。
鲍比必须猜出顶点顺(逆)时针的标号序列,任意一个符合条件的序列即可。
原题请见附录
例如:
n=5,给出的边或对角线是(1,4),(4,2),(1,2),(5,1),(2,5),(5,3),(1,3)。
那么一个可能的顶点标号顺序为(1,3,5,2,4)。
对应的多边形如图3-1:
**建立模型
根据题目意思,不难得到它的图论模型:
凸多边形对应了一个有n个顶点的图G,多边形的边和对角线对应着图G中的边。
而且十分明显的是,图G是一个平面图,根据欧拉公式,图中边的数量级为O(n)。
研究图G的平面图性质可以发现结论3.1:
一条对角线将凸多边形分成了两部分,每部分都至少含有一个除对角线外顶点,而且这两部分分居在对角线的两侧。
由此可知,在图G中只存在惟一的一条哈密顿回路。
因为对角线会将多边形分成不相关连的两部分,所以对角线不可能存于哈密顿回路上。
因此哈密顿回路上的边都是由多边形上的边组成,而多边形的边只有n条,可知哈密顿回路也就只有一条。
不妨设这条哈密顿回路为H1,H2,H3……Hn。
问题的目标就是要找到这个哈密顿回路。
下面讨论如何利用这些性质来设计算法。
**利用边的连通性
由结论3.1可知,如果一条边是对角线,那么将对角线的两个端点从图G中删除,图G一定会变成两个互不可达的连通分块;而如果一条边是多边形上的边,那么将这条边的两个端点删除,图G将仍然是连通的。
也就是说能够根据图G中边的连通性,来判断一条边是对角线还是边。
因此,得到算法一:
算法一:
1.枚举图中的每条边(u,v),进行如下判断:
将u和v两个顶点从图G中删除得到新图G’。
在新图G’任选一点a,然后从a开始对图G’进行遍历。
如果a能够到达图G’中的其它点,那么就说明图G’是连通的,(u,v)是多边形的边;否则,图G’不连通,(u,v)是多边形的对角线。
2.将图G中所有对角线删除,得到新图C。
这时的新图C便是图G中的哈密顿回路,因此只要对其进行一次遍历就可以得到多边形顶点的标号序列了。
分析一下算法一的复杂度:
步骤1中,要枚举的边最多为2n条,每判断一次图的连通性为O(n),所以复杂度为O(n2);步骤2中,删除边和遍历新图的复杂度都为O(n)。
所以总的复杂度为O(n2)。
n最大时候达到10000,因此算法一的复杂度稍稍高了一点,仍要继续优化。
**分而治之
继续挖掘图G的性质发现,由于图G中的对角线是不相交的,那么必定存在一个顶点u,它的度数为2。
而且可以肯定的是,和u相连的两条边一定是多边形的边。
将以u为顶点的三角形从多边形上删除,剩下的图形仍然是一个多边形。
是否能用这个性质判断一条边是多边形的边还是对角线呢?
答案是肯定的。
由于图中一定存在一个顶点u,它的度数为2。
设u是哈密顿回路上的顶点Hi,其相邻的两个顶点为Hi-1和Hi+1。
而(Hi-1,Hi)和(Hi,Hi+1)两条边都是多边形的边,将这两条边添加到一个附加图C中(附加图C一开始只有n个顶点,对应着多边形的顶点,顶点之间没有边相连)。
将以u为顶点的三角形(即三角形HiHi-1Hi+1)从多边形上砍掉之后,剩下的图形仍然是一个多边形。
也就是说,可以把Hi从图G中删除,若Hi-1和Hi+1之间没有边相连,就添加一条新边(Hi-1,Hi+1)(虚边),得到新图G’。
这时,新图G’中仍唯一存在一条哈密顿回路……Hi-2,Hi-1,Hi+1,Hi+2……。
图G’和图G具有同样的性质,因此也至少存在一个度为2的点,令其为Hj。
那么(Hj-1,Hj)和(Hj,Hj+1)这两条边有可能为多边形的边、多边形的对角线或者新添加的虚边,将其中多边形的边添加到图C中。
然后按照同样的方法把Hj从图G’删除,得到一个新图……。
以此类推,不断地从新图中找到度为2的点,然后将其相应的两条边中是多边形的边添加入图C中,接着从图中删除这个点。
如此反复,直到图中不存在度为2的点。
然后把图中剩下的边中,是多边形的边的添加到图C中。
最后,图C便是要求的原始多边形。
样例的模拟过程如下:
还存在一个问题没有解决:
如何判断移除的边是否是原多边形的边呢?
首先考虑一下,一条边(Hi,Hj)什么时候才会被移除。
一条边(Hi,Hj)当且仅当成为某个多边形的边时,它才有可能被移除。
如果(Hi,Hj)是原始多边形的一条对角线,当且仅当哈密顿回路上从Hi+1到Hj-1的顶点都已经被删除,(Hi,Hj)才有可能成为新多边形的边。
如何判断从Hi+1到Hj-1的顶点是否已经被删除呢?
这时构造的附加图C就起到了作用!
若从Hi+1到Hj-1的顶点已经被删除,则这些顶点在原多边形上相应的边都已经添加到图C中,Hi到Hj在图C中一定存在一条路径。
因此判断一条移除的边(Hi,Hj)是否是对角线,只需要判断在图C中Hi和Hj是否连通。
值得注意的是,当图C中已经有了n-1条边时,剩下的那条边会被判断成对角线,但此时已经能够确定多边形顶点的标号序列了。
算法的大致轮廓已经清楚了,下面为算法选择合适的数据结构:
a.图G的存储结构:
由于图G是稀疏图,可以用邻接表存储,用来查找度为2的顶点与哪些边相连。
还要用一个哈希表存下所有的边,用于查找任意两个点是否相连。
设一条边为(u,v),哈希表可以用
作为关键字,哈希函数为
,P为大质数。
这样在保证查找复杂度仍然是O
(1)的情况下,存储空间比邻接矩阵小了很多。
b.枚举度为2的结点:
很容易想到用最小堆来找出度为2的顶点,不过这样的复杂度稍稍高了一点。
由于顶点的度数只减少不增加,而且真正有效的度数只有1和2,所以可以建立四个桶来替代堆。
将顶点按度数分别放到四个不同的桶中:
度数为0、度数为1、度数为2、度数大于2。
在每个桶中用双向链接表存储下不同的结点。
当一个顶点的度数被修改的时候,从原桶中删除,放到相应的桶中。
在双向链表中的插入和删除结点,时间复杂度都是O
(1)。
而因为每个结点的度数是不断减少的,所以每个结点最多进出每个桶一次。
综上所述,在这些桶中查找和调整一个度为2的结点所需的时间复杂度为O
(1)。
c.在图C中添加边,判断任意两点的连通性:
可以采用并查集来实现边的添加(即将两个连通子图合并)和判断点的连通性(即判断两个点是否在同一个连通子图中)。
添加一条边和判断一对点的连通性两种操作的均摊复杂度为O(α(n))。
下面是对算法二的总括:
算法二
1.根据每个顶点的度数将n个顶点放到四个桶中。
初始化附加图C。
2.如果存在一个度数为2的顶点u,则接下来执行第3步,否则转步骤6。
3.在邻接表中找到和u相连的两个顶点为v1和v2。
4.首先判断(u,v1)、(u,v2)是否为虚边。
若不是虚边,用并查集检查其是否为对角线。
最后将是多边形的边插入到图C中。
5.将顶点u标记为不可用。
用哈希表检查边(v1,v2)是否存在图G中。
如果边(v1,v2)不存在,则添加一条虚边(v1,v2),v1和v2的度数不变;否则将v1和v2的度数减一。
若v1或v2修改后的度数不符合所在桶的性质,则进行调整。
然后转到步骤2。
6.找到残图G中所有度数为1的顶点,一一检查这些顶点相应的边在附加图C中的连通性,把非对角线和虚边的边插入到图C中。
7.遍历图C得到原始多边形顶点的标号顺序。
在每个顶点删除时,最多添加一条虚边,因此原始边和虚边的总数仍然是O(n)的级别。
现在分析一下时间复杂度:
步骤3中,每个顶点最多查找一次,因此复杂度为边数,即O(n);步骤4和步骤6中,用并查集查找和插入的均摊时间复杂度为O(nα(n));步骤5中,用哈希表查找一条边的复杂度为O
(1),边的插入和桶的调整也是O
(1),一共进行顶点数次,所以时间复杂度为O(n)。
综上所述,算法二的复杂度为O(nα(n))。
算法二的时间复杂度已经十分低了。
但追求无止境的我们不能浅尝辄止,因为算法二仍存在需待改进的地方:
编程复杂度实在太高了!
算法二虽然已经较为充分的挖掘出问题的各种特性,但在利用这些特性时,是通过不断分解成子问题然后一一击破的方法来实现的。
我们不禁提出一个问题:
是否能找到一种综合的方法,将所有特性一起利用,使得解决方法既简便又高效呢?
**DFS树反映的问题特性
回到多边形所在的平面图上。
若按深度优先来遍历平面图,当经过一条对角线时,平面图便被分成了两个部分。
由于是深度优先遍历,那么在这两部分剩下的点中,有一部分的点会被优先遍历到。
因此可以模仿DFS寻找块的算法,利用顶点的遍历顺序和low函数来判断边的性质。
首先定义一下DFS中需要用到的两个函数:
dfn[v]表示v是深搜中第几个被遍历到的。
其中w1表示v的儿子结点,w2表示DFS树中异于v的父亲结点的其他祖先结点。
接下来分情况讨论如何用dfn和low函数判断一条边的性质:
考虑DFS树上的一条边(u,v),其中u是v的父亲结点。
由于图G是一个块,因此每个点的儿子个数不会超过2。
根据v的度数分下面四种情况:
1.v的儿子数为0:
意味着(u,v)是原始多边形的边。
否则的话,v所有的祖先结点都在(u,v)的一侧,而另一侧一定仍有点存在,与儿子数为0矛盾。
令x是与v直接相连的祖先结点中dfn值最小的一个结点,那么(x,v)一定也是原始多边形的边。
略证:
如图3-3,另(x,v)为多边形的边。
v不是x的父亲结点,说明x一定先于u被遍历到,而且x是u的祖先结点。
也就是说,x到u存在一条路径,路径上的边都是由DFS树上的边组成。
令y为异于x和u,而与v相连的点。
由于图G是平面图,那么y一定在x到u路径上。
因此y一定后于x被遍历,即dfn[y]小于dfn[x]。
所以x一定是与v相连的祖先结点中dfn值最小的。
2.v的儿子数为1,且u为DFS树的根:
易知,(u,v)一定是多边形的边。
3.v的儿子数为1,且u不是DFS树的根:
令w为v惟一的儿子。
若low[w]大于等于dfn[u],则(u,v)为多边形的对角线。
因为(u,v)将平面分成了两部分,w和其子树结点是遍历不到另一部分的,如图3-4。
令x是与v直接相连的祖先结点中dfn值最小的一个结点,那么(x,v)一定是原始多边形的边(证明同情况1中的分析)。
而与v相连的另一条多边形的边可以在遍历w时找到。
若low[w]小于dfn[u],则(u,v)是多边形的边,而与v相连的另一条多边形的边可以在遍历w时找到。
4.v的儿子数为2,则与v相连的两条多边形的边可以在遍历儿子结点时找到。
考虑上述四种情况已经能够判断图G中所有边的性质了。
因此只需要对图G进行一次深度优先遍历,就能确定多边形顶点的标号顺序。
所以算法三的时间复杂度为线性的——O(n)!
**小结
上述三种方法都是建立在同一个模型上的,不同的方法对模型性质挖掘的深度不同也就决定了不同的时间复杂度。
算法一运用了定义法,利用对角线将多边形划分成两部分的性质找到多边形中所有的对角线。
而在算法二的设计过程中,发现了每次删除多边形的一个角时仍是一个多边形,也就是找到了子问题的相似性,这样就可以不断地缩小问题的规模。
算法二还体现了一种化归的思想,将未知问题分成几个步骤(或者子问题),每个步骤都是我们熟知的,因此可以为每一步选择最好的算法。
由于每个子问题都解决了,原问题也就迎刃而解。
可以说方法二是一种分析法。
算法三体现了一种综合思想,不是单独考虑每一条边的连通性,而是从全局考虑,发现了多边形的边和对角线在DFS树中的区别。
因此算法三的设计一气呵成,体现了图论中一种“序”的优美性。
三种不同的方法也给了我们不同的启示:
定义法告诉我们,当找不到问题思考的方向时,可以考虑从问题最基本的性质入手寻找突破口。
分析法的好处是,分解后的子问题一般比原问题要简单,困难的是每一