学习一个支配树

http://blog.csdn.net/qq_35649707/article/details/64125918
http://blog.csdn.net/GEOTCBRL/article/details/57875070
http://blog.csdn.net/a710128/article/details/49913553

1、基本介绍 支配树 DominatorTree

  对于一个流程图(单源有向图)上的每个点w,都存在点d满足去掉d之后起点无法到达w,我们称作d支配w,d是w的一个支配点。
这里写图片描述
  支配w的点可以有多个,但是至少会有一个。显然,对于起点以外的点,它们都有两个平凡的支配点,一个是自己,一个是起点。在支配w的点中,如果一个支配点i≠w满足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,满足v≤w,那么任意v到w的路径经过v,w的公共祖先。(注意这里不是说LCA)
  证明:如果v,w其中一个是另一个的祖先显然成立。否则删掉起点到LCA路径上的所有点(这些点是v,w的公共祖先),那么v和w在两棵子树内,并且因为公共祖先被删去,无法通过后向边到达子树外面,前向边也无法跨越子树,而横叉边只能从大到小,所以从v出发不能离开这颗子树到达w。所以如果本来v能够到达w,就说明这些路径必须经过v,w的公共祖先。

  在继续之前,我们先约定一些记号
  V代表图的点集,E代表图的边集。
  a→b代表从点a直接经过一条边到达点b,
  a⇝b代表从点a经过某条路径到达点b,
  a→˙b代表从点a经过DFS树上的树边到达点b(a是b在DFS树上的祖先),
  a→+b代表a→˙b且a≠b。

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

  对于w≠r,它的半支配点定义为sdom(w)=min{v|∃(v0,v1,⋯,vk−1,vk),v0=v,vk=w,∀1≤i≤k−1,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

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

引理3

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

引理4

  对于任意w≠r,有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

  对于任意w≠r,如果所有满足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,因为xx是小于sdom(w)的最后一个,所以v也满足sdom(w)→˙v→˙w,但是我们取的y已经是最小的一个了,矛盾。于是y只能是sdom(w),那么我们就证明了对于任意路径都要经过sdom(w),所以sdom(w)就是idom(w)。

定理3

  对于任意w≠r,令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

   对于w≠r,令u为所有满足sdom(w)→+u→˙w的u中sdom(u) 最小的一个,有idom(w)={sdom(w)idom(u)(sdom(u)=sdom(w))(sdom(u)

定理4

  对于任意w≠r,sdom(w)=min({v|(v,w)∈E,v

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是一致的。

  然后把该点加入它的sdoms的bucket 里,连上它与父亲的边。现在它父亲到它的这棵子树中已经处理完了,所以可以对父亲的bucket 里的每个点求一次sdom并且清空bucket。对于bucket 里的每个点vv,求出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转成真实编号的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值