快速构造支配树的Lengauer-Tarjan算法

本篇口胡写给我自己这样的老是证错东西的口胡选手 以及那些想学支配树,又不想啃论文原文的人…

  大概会讲的东西是求支配树时需要用到的一些性质,以及构造支配树的算法实现…

  最后讲一下把只有路径压缩的并查集卡到 O(mlogn) O(mlog⁡n)上界的办法作为小彩蛋…

 

1、基本介绍 支配树 DominatorTree

  对于一个流程图(单源有向图)上的每个点 w ,都存在点 d 满足去掉 d 之后起点无法到达 w ,我们称作 d 支配 w d w 的一个支配点。

  

  支配 w 的点可以有多个,但是至少会有一个。显然,对于起点以外的点,它们都有两个平凡的支配点,一个是自己,一个是起点。

  在支配 w 的点中,如果一个支配点 iw 满足 i w 剩下的所有支配点支配,则这个 i 称作 w 的最近支配点(immediate dominator),记作
idom(w)

  定理1:我们把图的起点称作 r ,除 r 以外每个点均存在唯一的 idom

  这个的证明很简单:如果 a 支配 b b 支配 c ,则 a 一定支配 c ,因为到达 c 的路径都经过了 b 所以必须经过 a ;如果 b 支配 c a 支配 c ,则 a 支配 b (或者 b 支配 a ),否则存在从 r b 再到 c 的路径绕过 a ,与 a 支配 c 矛盾。这就意味着支配定义了点 w 的支配点集合上的一个全序关系,所以一定可以找到一个“最小”的元素使得所有元素都支配它。

  于是,连上所有 r 以外的 idom(w)w 的边,就能得到一棵树,其中每个点支配它子树中的所有点,它就是支配树。

  

  支配树有很多食用…哦不…是实际用途。比如它展示了一个信息传递网络的关键点,如果一个点支配了很多点,那么这个点的传递效率和稳定性要求都会很高。比如Java的内存分析工具(Memory Analyzer Tool)里面就可以查看对象间引用关系的支配树…很多分析上支配树都是一个重要的参考。

  为了能够求出支配树,我们下面来介绍一下需要用到的基本性质。

 

2、支配树相关性质

  首先,我们会使用一棵DFS树来帮助我们计算。从起点出发进行DFS就可以得到一棵DFS树。

  

  观察上面这幅图,我们可以注意到原图中的边被分为了几类。在DFS树上出现的边称作树边,剩下的边称为非树边。非树边也可以分为几类,从祖先指向后代(前向边),从后代指向祖先(后向边),从一棵子树內指向另一棵子树内(横叉边)。树边是我们非常熟悉的,所以着重考虑一下非树边。

  我们按照DFS到的先后顺序给点从小到大编号(在下面的内容中我们通过这个比较两个节点),那么前向边总是由编号小的指向编号大的,后向边总是由大指向小,横叉边也总是由大指向小。现在在DFS树上我们要证明一些重要的引理:

 


 

  引理1(路径引理):

    如果两个点 v,w ,满足 vw ,那么任意 v w 的路径经过 v,w 的公共祖先。(注意这里不是说LCA)

  证明:

    如果 v,w 其中一个是另一个的祖先显然成立。否则删掉起点到LCA路径上的所有点(这些点是 v,w 的公共祖先),那么 v w 在两棵子树内,并且因为公共祖先被删去,无法通过后向边到达子树外面,前向边也无法跨越子树,而横叉边只能从大到小,所以从 v 出发不能离开这颗子树到达 w 。所以如果本来 v 能够到达 w ,就说明这些路径必须经过 v,w 的公共祖先。

 


 

  在继续之前,我们先约定一些记号:

   V 代表图的点集, E 代表图的边集。

   ab 代表从点 a 直接经过一条边到达点 b

   ab 代表从点 a 经过某条路径到达点 b

   a˙b 代表从点 a 经过DFS树上的树边到达点 b a b 在DFS树上的祖先),

   a+b 代表 a˙b ab

  


 

  定义 半支配点(semi-dominator):

    对于 wr ,它的半支配点定义为 sdom(w)=min{v|(v0,v1,,vk1,vk),v0=v,vk=w,1ik1,vi>w}

  对于这个定义的理解其实就是从 v 出发,绕过 w 之前的所有点到达 w 。(只能以它之后的点作为落脚点)

  注意这只是个辅助定义,并不是真正的支配点。甚至在只保留 w w 以前的点时它都不一定是支配点。例子: V={1,2,3,4},E={(1,2),(2,3),(3,4),(1,3),(2,4)},r=1,sdom(4)=2               ,但是 2 不支配 4 。不过它代表了有潜力成为支配点的点,在后面我们可以看到,所有的 idom 都来自自己或者另一个点的 sdom

 


 

  引理2

    对于任意 wr ,有 idom(w)+w

  证明很显然,如果不是这样的话就可以直接通过树边不经过 idom(w) 就到达 w 了,与 idom 定义矛盾。

 


 

  引理3

    对于任意 wr ,有 sdom(w)+w

  证明:

    对于 w 在DFS树上的父亲 faw faww 这条路径只有两个点,所以满足 sdom 定义中的条件,于是它是 sdom(w) 的一个候选。所以 sdom(w)faw 在这里我们就可以使用路径引理证明 sdom(w) 不可能在另一棵子树,因为如果是那样的话就会经过 sdom(w) w 的一个公共祖先,公共祖先的编号一定小于 w ,所以不可行。于是 sdom(w) 就是 w 的真祖先。

 


 

  引理4

    对于任意 wr ,有 idom(w)˙sdom(w)

  证明:

    如果不是这样的话,按照 sdom 的定义,就会有一条路径是 r˙sdom(w)w 不经过 idom(w) 了,与 idom 定义矛盾。

 


 

  引理5

    对于满足 v˙w 的点 v,w v˙idom(w) idom(w)˙idom(v)

  (不严谨地说就是 idom(w) w 的路径不相交或者被完全包含,其实 idom(w) 这个位置是可能相交的)

  证明:

    如果不是这样的话,就是 idom(v)+idom(w)+v+w ,那么存在路径 r˙idom(v)v+w 不经过 idom(w) 到达了 w (因为 idom(w) idom(v) 的真后代,一定不支配 v ,所以存在绕过 idom(w) 到达 v 的路径),矛盾。

 


 

  上面这5条引理都比较简单,不过是非常重要的性质。接下来我们要证明几个定理,它们揭示了 idom sdom 的关系。证明可能会比上面的复杂一点。

 


 

  定理2

    对于任意 wr ,如果所有满足 sdom(w)+u˙w u 也满足 sdom(u)sdom(w) ,那么 idom(w)=sdom(w)

  

sdom(w)˙sdom(u)+u˙w

  证明:

    由上面的引理4知道 idom(w)˙sdom(w) ,所以只要证明 sdom(w) 支配 w 就可以保证是最近支配点了。对任意 r w 的路径,取上面最后一个编号小于 sdom(w) x (如果 sdom 就是 r 的话显然定理成立),它必然有个后继 y 满足 sdom(w)˙y˙w (否则 x 会变成 sdom(w) ,我们取最小的那个 y 。同时,如果 y 不是 sdom(w) ,根据条件, sdom(y)sdom(w) ,所以 x 不可能是 sdom(y) ,这就意味着 x y 的路径上一定有一个 v 满足 x+v+y ,因为 x x是小于 sdom(w) 的最后一个,所以 v 也满足 sdom(w)˙v˙w ,但是我们取的 y 已经是最小的一个了,矛盾。于是 y 只能是 sdom(w) ,那么我们就证明了对于任意路径都要经过 sdom(w) ,所以 sdom(w) 就是 idom(w)

 


 

  定理3

    对于任意 wr ,令 u 为所有满足 sdom(w)+u˙w u sdom(u) 最小的一个,那么 sdom(u)sdom(w)idom(w)=idom(u)

  

sdom(u)˙sdom(w)+u˙w

  证明:

    由引理5,有 idom(w)˙idom(u) u˙idom(w) ,由引理4排除后面这种。所以只要证明 idom(u) 支配 w 即可。类似定理2的证明,我们取任意 r w 路径上最后一个小于 idom(u) x (如果 idom(u) r 的话显然定理成立),路径上必然有个后继 y 满足 idom(u)˙y˙w (否则 x 会变成 sdom(w) ),我们取最小的一个 y 。类似上面的证明,我们知道 x y 的路径上不能有点 v 满足 idom(u)˙v+y ,于是 x 成为 sdom(y) 的候选,所以 sdom(y)x 。那么根据条件我们也知道了 y 不能是 sdom(w) 的真后代,于是 y 满足 idom(u)˙y˙sdom(w)   。但是我们注意到因为 sdom(y)x ,存在一条路径 r˙sdom(y)y˙u  ,如果 y 不是 idom(u)  的话这就是一条绕过 idom(u)  的到 u 的路径,矛盾,所以 y 必定是 idom(u)  。所以任意到 w 的路径都经过 idom(u) ,所以 idom(w)=idom(u)  。

 


 

  幸苦地完成了上面两个定理的证明,我们就能够通过 sdom 求出 idom 了:


 

  推论1 

    对于 wr ,令 u 为所有满足 sdom(w)+u˙w u sdom(u)  最小的一个,有

    

idom(w)={sdom(w)idom(u)(sdom(u)=sdom(w))(sdom(u)<sdom(w))

  通过定理2和定理3可以直接得到。这里一定有 sdom(u)sdom(w) ,因为 w 也是 u 的候选。

 


 

  接下来我们的问题是,直接通过定义计算 sdom 很低效,我们需要更加高效的方法,所以我们证明下面这个定理:


 

  定理4

    对于任意 wr sdom(w)=min({v|(v,w)E,v<w}{sdom(u)|u>w,(v,w)E,u˙v})

  证明:

    令等号右侧为 x ,显然右侧的点集中都存在路径绕过 w 之前的点,所以 sdom(w)x  。然后我们考虑 sdom(w)   w  的绕过 w  之前的点的路径,如果只有一条边,那么必定满足 (sdom(w),w)E    sdom(w)<w ,所以此时 xsdom(w) ;如果多于一条边,令路径上 w 的上一个点为 last   ,我们取路径上除两端外满足 p˙last  的最小的 p (一定能取得这样的 p ,因为 last   是 p  的候选)。因为这个 p 是最小的,所以 sdom(w) p 的路径必定绕过了 p 之前的所有点,于是 sdom(w) sdom(p) 的候选,所以 sdom(p)sdom(w) 。同时, sdom(p) 还满足右侧的条件( p 在绕过 w 之前的点的路径上,于是 p>w ,并且 p˙last ,同时 last   直接连到了 w ),所以 sdom(p) x 的候选, xsdom(p) 。所以 xsdom(p)sdom(w,  xsdom(w) 。综上, sdom(w)x xsdom(w) ,所以 x=sdom(w)

 


 

  好啦,最困难的步骤已经完成了,我们得到了 sdom 的一个替代定义,而且这个定义里面的形式要简单得多。这种基本的树上操作我们是非常熟悉的,所以没有什么好担心的了。接下来就可以给出我们需要的算法了。

 

3、Lengauer-Tarjan算法

算法流程:

  1、初始化、跑一遍DFS得到DFS树和标号
  2、按标号从大到小求出 sdom (利用定理4)
  3、通过推论1求出所有能确定的 idom ,剩下的点记录下和哪个点的 idom  是相同的
  4、按照标号从小到大再跑一次,得到所有点的 idom

  很简单对不对~有了理论基础后算法就很显然了。

 

具体实现:

  大致要维护的东西:
   vertex(x)  标号为 x 的点 u
   pred(u)  有边直接连到 u 的点集
   parent(u)   u 在DFS树上的父亲 fau
   bucket(u)   sdom  为点 u 的点集
  以及 idom sdom 数组

  第1步没什么特别的,规规矩矩地DFS一次即可,同时初始化 sdom 为自己(这是为了实现方便)。

  第2、3步可以一起做。通过一个辅助数据结构维护一个森林,支持加入一条边( link(u,v) )和查询点到根路径上的点的 sdom 的最小值对应的点( eval(u) )。那么我们求每个点的 sdom 只需要对它的所有直接前驱 eval 一次,求得前驱中的 sdom 最小值即可。因为定理4中的第一类点编号比它小,它们还没有处理过,所以自己就是根, eval 就能取得它们的值;对于第二类点, eval  查询的就是满足 u˙v u sdom(u) 的最小值。所以这么做和定理4是一致的。

  然后把该点加入它的 sdom s bucket  里,连上它与父亲的边。现在它父亲到它的这棵子树中已经处理完了,所以可以对父亲的 bucket  里的每个点求一次 sdom 并且清空 bucket 。对于 bucket  里的每个点 v v,求出 eval(v) ,此时 parent(w)+eval(v)˙v ,于是直接按照推论1,如果 sdom(eval(v))=sdom(v) ,则 idom(v)=sdom(v)=parent(w) ;否则可以记下 idom(v)=idom(eval(v)) ,实现时我们可以写成 idom(v)=eval(v) ,留到第4步处理。
  最后从小到大扫一遍完成第4步,对于每个 u ,如果 idom(u)=sdom(u) 的话,就已经是第3步求出的正确的 idom 了,否则就证明这是第3步留下的待处理点,令 idom(u)=idom(idom(u)) 即可。

  对于这个辅助数据结构,我们可以选择并查集。不过因为我们需要查询到根路径上的信息,所以不方便写按秩合并,但是我们仍然可以路径压缩,压缩时保留路径上的最值就可以了,所以并查集操作的复杂度是 O(logn) 。这样做的话,最终的复杂度是 O(nlogn) 。(各种常见方法优化的并查集只要没有按秩合并就是做不到 α α的复杂度的,最下面我会提到如何卡路径压缩)

  原论文还提到了一个比较奥妙的实现方法,能够把这个并查集优化到 α α的复杂度,不过看上去比较迷,我觉得我会写错,所以就先放着了,如果有兴趣的话可以找原论文A Fast Algorithm for Finding Dominators in a Flowgraph,里面的参考文献14是Tarjan的另一篇东西Applications of Path Compression on Balanced Trees,原论文说用的是这里面的方法…等什么时候无聊想要真正地学习并查集的各种东西的时候再看吧…(我又挖了个大坑)

 

代码实现


















#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<string>
#include<algorithm>
#include<cmath>
#include<vector>
using namespace std;
inline int read()
{
	char ch=getchar();int i=0,f=1;
	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){i=(i<<3)+(i<<1)+ch-'0';ch=getchar();}
	return i*f;
}
const int N = 200010;
struct eg{ int to,before; }edge[N];
int n,m,tot,ecnt;
int last[N],pos[N],idx[N],fa[N],sdom[N],idom[N];
int father[N],val[N];
vector<int> pre[N],bkt[N];
int findf(int p)
{
	if(father[p]==p) return p;
	int r=findf(father[p]);
	if(sdom[val[father[p]]]<sdom[val[p]]) val[p] = val[father[p]];
	return father[p] = r;
}
inline int eval(int p)
{
	findf(p);
	return val[p];
}
void dfs(int p)
{
	idx[pos[p]=++tot]=p,sdom[p]=pos[p];
	for(int pt=last[p];pt;pt=edge[pt].before) if(!pos[edge[pt].to])
	dfs(edge[pt].to),fa[edge[pt].to]=p;
}
void work()
{
	int i,p;
	dfs(1);
	for(i=tot;i>=2;i--)
	{
		p=idx[i];
		for(int k:pre[p])
			if(pos[k])sdom[p]=min(sdom[p],sdom[eval(k)]);
		bkt[idx[sdom[p]]].push_back(p);
		int fp=fa[p];father[p]=fa[p];
		for(int v:bkt[fp])
		{
			int u = eval(v);
			idom[v] = sdom[u]==sdom[v]?fp:u;
		}
		bkt[fp].clear();
	}
	for(i=2;i<=tot;i++) p=idx[i],idom[p]=(idom[p]==idx[sdom[p]])?idom[p]:idom[idom[p]];
	for(i=2;i<=tot;i++) p=idx[i],sdom[p]=idx[sdom[p]];
}
inline void link(int a,int b)
{
	edge[++ecnt].to=b,edge[ecnt].before=last[a],last[a]=ecnt;
	pre[b].push_back(a);
}
int main()
{
	int i;
	n=read(),m=read();
	tot=ecnt=0;
	for(i=1;i<=n;i++)last[i]=pos[i]=0,father[i]=val[i]=i,pre[i].clear(),bkt[i].clear();
	for(i=1;i<=m;i++)
	{
		int a=read(); 
		link(a,read()); 
	}
	work();
	return 0;
}

  我的变量名都很迷…不要在意…(它们可是经过了长时间的结合中文+英文+象形+脑洞的演变得出的结果)

  稍微需要注意一下的就是实现时点的真实编号和DFS序中的编号的区别,DFS序的编号是用来比较的那个。以及尽量要保持一致性(要么都用真实编号,要么都用DFS序编号),否则很容易写错…我的这段代码里 idom 用的是真实编号, sdom 用的是DFS序编号,最后再跑一次把 sdom 转成真实编号的。

 

4、欢快的彩蛋 卡并查集!

  是不是听到周围有人说:“我的并查集只写了路径压缩,它是单次操作 α 的”。这时你要坚定你的信念,你要相信这是 O(logn) 的。如果他告诉你这个卡不了的话…你或许会觉得确实很难卡…我也觉得很难卡…但是Tarjan总知道怎么卡。

  现在确认一下纯路径压缩并查集的实现方法:每次基本操作 find(v) 后都把 v 到根路径上的所有点直接接在根的下面,每次合并操作对需要合并的两个点执行 find 找到它们的根。

  看起来挺优的。(其实真的挺优的,只是没有 α 那么优)

  Tarjan的卡法基于一种特殊定义的二项树(和一般的二项树的定义不同)。

  定义这种特殊的二项树 Tk Tk为一类多叉树,其中 T1,T2,,Tj 都是一个单独的点,对于 Tk,k>j Tk 就是 Tk1 再接上一个 Tkj 作为它的儿子。

  

  就像这样。这种定义有一个有趣的特性,如果我们把它继续展开,可以得到各种有趣的结果。比如我们把上面图中的 Tkj 继续展开,就会变成 Tkj1 接着 Tk2j ,以此类推可以展开出一串。而如果对 Tk1 继续展开,父节点就会变成 Tk2 Tk−2,子节点多出一个 Tkj1 ,以此类推可以展开成一层树。下面的图展示了展开 Tk 的不同方式。

  

  让我们好好考虑一下这意味着什么。从图4到图5…除了这些树的编号没有对应上以外,会不会有一种感觉,图5像是图4路径压缩后的结果。

  图4的展开方式中编号的间隔都是 j ,图5的展开方式中间隔都是 1 …那么如果我们用图5的方式展开出 j 棵子树,再按图4展开会怎么样呢?(假设 j 整除 k

  

  变成了这个样子,就确实和路径压缩扯上关系了。如果在最顶上再加一个点,然后 j j次访问底层的 T1,T2,,Tj 就可以把树压成图5的样子了,不过会多一个单点的儿子出来,因为图6中其实有两个 Tj (因为图4展开到最后一层没有了 1 ,所以会和上一层出现一次重复)。这么一来,我们又可以做一次这一系列操作了,非常神奇!(原论文里把这个叫做self-reproduction)至于 Tk Tk的实际点数,通过归纳法可以得到点数不超过 (j+1)kj1 。(我们只对能被 j j整除的 k 进行计算,每次 j 次展开父节点进行归纳)

 有了这个我们就有信心卡纯路径压缩并查集了。令 m 代表询问操作数, n 代表合并操作数,不妨设 mn ,我们取 j=mn,i=logj+1n2+1,k=ij那么 Tk 的大小不超过 (j+1)i1 n2 。接下来我们做 n2 组操作,每组在最顶上加入一个点,然后对底层的 j j个节点逐一查询,每次查询的路径长度都是 i+1 i+1。同时总共的查询次数还是不超过 m m。于是总共的复杂度是 n2j(i+1)=Ω(mlog1+m/nn)

  Boom~爆炸了,所以它确实是 log 级的。

  彩蛋到这里就结束啦…如果想知道更多并查集优化方法怎么卡,可以去看这一部分参考的原论文Worst-Case Analysis of Set Union Algorithms,里面还附带了一个表,有写各种并查集实现不带按秩合并和带按秩合并的复杂度,嗯,卡并查集还是挺有趣的(只是一般人想不到呀…Tarjan太强辣)…

 

  (题外话:这次我画了好多图,感觉自己好良心呀w 其实都是对着论文上的例子画的)



from:http://www.cnblogs.com/meowww/p/6475952.html


  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值