zoukankan      html  css  js  c++  java
  • 支配树学习笔记

    支配树(dominator tree) 学习笔记

    学习背景

    本来本蒟蒻都不知道有一个东西叫支配树……pkuwc前查某位的水表看见它的大名,甚感恐慌啊。不过好在pkuwc5道题(嗯?)都是概率期望计数,也不知是好还是不好,我在这些方面也只是不好不差……扯远了。

    考挂之后也没什么心思干别的,想起支配树这个东西,于是打算学一下。

     

    技能介绍(雾)

    支配树是什么?不如直接讲支配树的性质,从性质分析它的定义。

    先大概讲一下它是来求什么的。

    问题:我们有一个有向图(可以有环),定下了一个节点为起点s。现在我们要求:从起点s出发,走向一个点p的所有路径中,必须要经过的点有哪些{xp}。

    换言之,删掉{xp}中的任意一个点xpi以及它的入边出边,都会使s无法到达p。

    我们有一种显然的O(nm)的方法:枚举+BFS。

    现在我们学习构造图的支配树,它是一种复杂度更优秀的做法。

    性质:

    1. 它是一棵树(这不废话),根节点是我们选定的起点s。
    2. 对于每个点i,它到根的链上的点集就是对于它的必经点集{xi}。
    3. 对于每个点i,它是它的支配树上的子树内的点的必经点。

    所以对于上面的问题,把支配树抠出来就可以了。

     

    算法原理

    先来看一下两种比较简单的情况。不妨假设从s出发可以到达图的所有点,不失一般性。

    显而易见的,树就是自己的支配树……

    有向无环图(DAG)

    DAG上的问题当然要靠拓扑序来搞!

    我们利用拓扑序做。对于一个点,所有能到达它的点在支配树中的lca,就是它支配树中的父亲。

    用倍增求lca可以做到O(nlogn)。

    比如说 ZJOI2012 灾难

    答案就是支配树上的size。

    当时这道题好像也挺难……谁能想到新建树啊……

     

    一般有向图

    注:下面的一切涉及大小的都是用dfn做比较的,不然太丑了……

    显然支配具有传递性。

    先随便搞出一棵dfs树,用dfn[x]表示x在dfs序的哪里。

    dfs树一个重要性质:若v,w是图中节点且dfn[v]<=dfn[w],则任意从v到w的路径必然包含它们在dfs树中的一个公共祖先。

    定义:semi[x]叫x的半支配点。定义如下:

    semi[x]=min{v | 有路径v=v0, v1, ..., vk=x使得dfn[vi]>dfn[x]对1<=i<=k-1成立}.(掐头去尾,都走的dfn大于它的点)

    当然中间没有点的话semi[x]就是它dfs树上的父亲。

    semi有一些性质,具体可以参见这道题:cogs2117 DAGCH,解法在下面给出。

    题中的superior vertex就是semi。

    定义:idom[x]表示支配x的点中深度最深的点,叫x的支配点,也叫idom[x]支配了x。idom[x]就是x在支配树上的父亲。

    显然有下面的性质:

      1. 每个点的半支配点是唯一的。
      2. 一个点的半支配点必定是它在dfs树上的祖先,dfn[semi[x]]<dfn[x]。
      3. 半支配点不一定是x的支配点。
      4. semi[x]的深度不小于idom[x]的深度,即idom[x]在semi[x]的祖先链上。
      5. 设节点v,w满足v->w。则v->idom[w]或者idom[w]->idom[v](a->b表示a在b的祖先链上)。

    性质5证明:设x是idom[w]的一个完全后代,且同时是v的完全祖先,是idom[v]的后代。则必然有一条从s到v不经过x的路径。将这条路径和从v到w的树上路径连接起来,我们就得到了一条从s到w不经过x的路径,矛盾。因此idom[w]要么是v的后代,要么是v的祖先,就要是idom[v]的祖先。

    求出semi之后我们把dfs树上的点保留,和边(semi[i] -> i)。

    现在这张图已经是一个DAG了,显然已经可以用上面的方法写。

    但是你已经求出了semi,求idom就有种更快的方法。(semi怎么求后面有讲)

    定理:idom[x]和semi[x]的关系(如何用semi[x]优雅地得到idom[x])

    • 定义集合{P}表示dfs树中路径(semi[x],x)上的点集(不包括semi[x])。
    • 找到{P}中semi的dfn最小的点,记为z。
    • 如果z的semi和x的一样,则idom[x]=semi[x]。
    • 否则 idom[x]=idom[z]。

    对黑字的一些理解:

     

    第一行。

    • 由性质4,只要证明semi[x]支配了x就可以了。(感性一下还是很好证明的?)
    • 考虑一条(s => x)的链,设w是链上最后一个w<=semi[x]的点。如果不存在,那么就肯定支配了。
    • 设y是w后第一个y>=semi[x]的点。则有semi[x]<=y<x;
    • 来看一下路径(w => y) = {w,p1,p2,p3,……,pk,y},一定有pi>y。
    • 证明:若pi<y,则dfs树就会变成pi->y->x而不是semi[x]->x了。
    • 于是有semi[y]<=w,因为由semi定义w可能是y的半支配点。
    • 又因为w<semi[x] 所以semi[y]<=semi[x]。
    • 又由有y->x的链,所以semi[x]<=semi[y]。
    • 因为y不是semi[x]的完全后代,所以y=semi[x]就顺理成章了。
    • 因为链是任意的,所以semi[x]支配了x。

    第二行

    • 首先一定有idom[z]<=semi[x]<=z<=x。
    • 由性质2和性质4,idom[x]一定是z的完全祖先。
    • 再综合一下性质5,可以否定第二种情况idom[z]->idom[idom[x]],只存在idom[x]->idom[z]。所以只要证明idom[z]支配了x,就可以证明idom[z]=idom[x]。
    • 还是一样的,我们考虑一条链(s=>x),同样设w是链上最后一个w<=semi[x]的点。如果不存在,那么就肯定支配了。
    • 同样设y是w后第一个y>=semi[x]的点。则有semi[x]<=y<x,idom[z]<=y<=z<=x;
    • 同样看路径(w => y) = {w,p1,p2,p3,……,pk,y},一定有pi>y。证明同上。
    • 所以依旧有semi[y]<=w。
    • 由性质4,可得不等式semi[y]<=w<=idom[z]<=semi[z]。
    • 因为y不是semi[x]的完全后代,且y不可能既是z的祖先,又是idom[z]的完全后代,因为此时会有路径(s=>y)(不包含idom[z])+(y=>z)=(s=>z)但会避开idom[z],与idom[z]定义矛盾。
    • 由于idom[z]->y->z->x并且idom[z]->y->x,所以唯一的可能就是idom[z]=y。
    • 所以idom[z]必定位于s到x的路径上。因为路径是任意的,所以idom[z]支配了x。

    写这两点好累啊……

    (看不懂?没事,结论和代码都好背)

    很显然两行黑字包含了所有情况……

     

     

    那么如何用semi推idom我们已经知道了,下面就看如何求semi。

    比较大小同样按照dfn为准。

    定理:对任意节点y≠s,有点集{x|(x,y)∈E}。

    若x<y,则semi[y]=min(x)。

    若x>y,则semi[y]=min({semi[z]|z>y且存在链z->y})。

     

    这个的证明……很骚……真的很骚……

    定理可以简化为:semi[y]=min({x|(x,y)∈E} ∪ {semi[z] | z>y,z->x,(x,y)∈E})

    证明:令g=等式右边。

    证1:semi[y]<=g。

    如果是(g,y)∈E,根据semi定义,semi[y]至多是g,等式成立。

    如果是第二种情况,则g=semi[z],z>y,z->x,(x,y)∈E。由semi定义,存在路径g=v0, v1, ..., vk=z使得vi>z对1<=i<=k-1成立

    而dfs树上的路径(z=v0,v1,v2,…,vk=y)满足vi>=z>y成立。所以路径(g=v0,v1,v2,…,vk=y)使得vi>y对1<=i<=k-1成立。

    所以g也可以做y的semi,semi[y]<=g。

    证2:semi[y]>=g。

    图中肯定存在这么一条路径 (semi[x]=v0,v1,v2,…,vk=y)使得vi>y对1<=i<=k-1成立。

    若k=1,则(g,x)∈E,在第一种情况内。

    若k>1,设w是dfn[w]>1且存在(w=>vk-1)的最小值,很明显它一定存在。

    很显然对于1<=i<=j-1,vi>vj(不然就选i了嘛)。

    所以semi[x]>=semi[vj]>=g,即semi[x]>=g。

    经过上面两番证明,semi[y]=g也是水到渠成的了。

    (还是看不懂?没关系,结论代码依然好背)

    附上上面那题的代码

    #include <iostream>
    #include <cstdio>
    #include <cstdlib>
    #include <algorithm>
    #include <cstring>
    #include <vector>
    #include <cmath>
    #include <map>
    #include <set>
    #define LL long long
    #define FILE "dagch"
    using namespace std;
    
    const int N = 200010;
    struct Node{int to,next;}E[N<<1];
    int n,m,q,head[N],tot,dfn[N],clo,rev[N],fa[N],semi[N],Ans[N];
    vector<int>G[N];
    struct Union_Merge_Set{
      int fa[N],Mi[N];
      inline void init(){
        for(int i=0;i<=n;++i)
          fa[i]=Mi[i]=semi[i]=i;
      }
      inline int find(int x){
        if(x==fa[x])return x;
        int fx=fa[x],y=find(fa[x]);
        if(dfn[semi[Mi[fx]]]<dfn[semi[Mi[x]]])Mi[x]=Mi[fx];
        return fa[x]=y;
      }
    }uset;
    
    inline int gi(){
      int x=0,res=1;char ch=getchar();
      while(ch>'9' || ch<'0')res^=ch=='-',ch=getchar();
      while(ch>='0'&&ch<='9')x=x*10+ch-48,ch=getchar();
      return res?x:-x;
    }
    
    inline void link(int u,int v){
      E[++tot]=(Node){v,head[u]};
      head[u]=tot;
    }
    
    inline void tarjan(int x){
      dfn[x]=++clo;rev[clo]=x;
      for(int i=0,j=G[x].size();i<j;++i)
        if(!fa[G[x][i]])
          fa[G[x][i]]=x,tarjan(G[x][i]);
    }
    
    inline void build(){
      for(int i=n;i>=2;--i){
        int y=rev[i],tmp=n;
        for(int e=head[y];e;e=E[e].next){
          int x=E[e].to;if(!dfn[x])continue;
          if(dfn[x]<dfn[y])tmp=min(tmp,dfn[x]);
          else uset.find(x),tmp=min(tmp,dfn[semi[uset.Mi[x]]]);
        }
        uset.fa[y]=fa[y];semi[y]=rev[tmp];
        Ans[rev[tmp]]++;
      }
    }
    
    inline void solve(){
      n=gi();m=gi();q=gi();fa[1]=1;
      for(int i=1;i<=m;++i){
        int u=gi(),v=gi();
        link(v,u);
        G[u].push_back(v);
      }
      uset.init();
      for(int i=1;i<=n;++i)
        if(G[i].size())
          sort(G[i].begin(),G[i].end());
      tarjan(1);build();
      for(int i=1;i<=q;++i)
        printf("%d ",Ans[gi()]);
      printf("
    ");
      for(int i=0;i<=n;++i){
        G[i].clear();head[i]=0;
        Ans[i]=semi[i]=fa[i]=0;
      }
      clo=tot=0;
    }
    
    int main(){
      freopen(FILE".in","r",stdin);
      freopen(FILE".out","w",stdout);
      int Case=gi();while(Case--)solve();
      fclose(stdin);fclose(stdout);
      return 0;
    }
    DAGCH

    具体实现

    算法名叫:Lengauer Tarjan算法,顾名思义是由Lengauer和Tarjan提出的(%Tarjan)。

    论文里说:快速支配点算法包含三个部分。

    第一步:对原图做一边dfs,找出dfs树不提。

    “首先,对输入的流程图G=(V,E,r)进行从r开始的深度优先搜索,并将图G中节点按照DFS访问顺序从1到n编号。DFS建立了一棵以r为根的生成树T,其节点以先根顺序编号。”

    第二步:计算半支配点。

    发现不管是求semi还是idom,我们都要知道:

    找到{P}中semi的dfn最小的点,记为z =min({semi[z]|z>y且存在链z->y})。

    这两玩意其实是一个东西,看上去并不好做?

    其实这个想想就会啦。

    注意到存在z>y的关系,可以考虑按照dfn从大往小搞。

    那么在做semi的时候,第一种边很好搞,第二种边呢?

    因为处理过的点都是z>y的,且在dfs树中后代结点的dfn总比祖先大。

    所以这个时候查询的x就是对应一条祖先链。

    操作1:查询点x的祖先链中semi的最小值。

    处理完之后我们自然要把x扔进图中。因为x是当前dfn最小的点,所以它会做某个块的根。

    操作2:给根以父亲。

    这个用带权并查集轻松搞定。

    (不会带权并查集的请移步此处QaQ)

    第三步:通过半支配点计算支配点。

    注意:semi考虑了根而idom时不要,所以我的处理方法是这样的:

    for(id = dfn_num to 2){
      y= (dfn=id的点);
      for( x| (x->y)∈E){
        work_semi(semi[y],x);
      }
      并查集:fa[y]=dfs树上的fa[y]
      y= (dfn=id-1的点);
      for( x| (semi[x]=y)){
        work_idom(idom[x],y);
      }
    }

    在(id-1)还没有被处理的时候把以它为semi的点的itom处理掉就好啦。

     

    相关题目

    HDU4694

    大意:以n为出发点,求每个点支配的点的编号和。

    就是个裸的支配树嘛……

    #include <iostream>
    #include <cstdio>
    #include <cstdlib>
    #include <algorithm>
    #include <cstring>
    #include <vector>
    #include <cmath>
    #include <map>
    #include <set>
    #define LL long long
    #define FILE "dominator_tree"
    using namespace std;
    
    const int N = 200010;
    struct Node{int to,next;};
    int n,m,dfn[N],clo,rev[N],f[N],semi[N],idom[N],Ans[N];
    
    inline int gi(){
      int x=0,res=1;char ch=getchar();
      while(ch>'9' || ch<'0')res^=ch=='-',ch=getchar();
      while(ch>='0'&&ch<='9')x=x*10+ch-48,ch=getchar();
      return res?x:-x;
    }
    
    struct Graph{
      Node E[N];int head[N],tot;
      inline void clear(){
        tot=0;
        for(int i=0;i<=n;++i)head[i]=0;
      }
      inline void link(int u,int v){
        E[++tot]=(Node){v,head[u]};head[u]=tot;
      }
    }pre,nxt,dom;
    
    struct uset{
      int fa[N],Mi[N];
      inline void init(){
        for(int i=1;i<=n;++i)
          fa[i]=Mi[i]=semi[i]=i;
      }
      inline int find(int x){
        if(fa[x]==x)return x;
        int fx=fa[x],y=find(fa[x]);
        if(dfn[semi[Mi[fx]]]<dfn[semi[Mi[x]]])Mi[x]=Mi[fx];
        return fa[x]=y;
      }
    }uset;
    
    inline void tarjan(int x){
      dfn[x]=++clo;rev[clo]=x;
      for(int e=nxt.head[x];e;e=nxt.E[e].next){
        if(!dfn[nxt.E[e].to])
          f[nxt.E[e].to]=x,tarjan(nxt.E[e].to);
      }
    }
    
    inline void dfs(int x,int sum){
      Ans[x]=sum+x;
      for(int e=dom.head[x];e;e=dom.E[e].next)
        dfs(dom.E[e].to,sum+x);
    }
    
    inline void calc(){
      for(int i=n;i>=2;--i){
        int y=rev[i],tmp=n;
        for(int e=pre.head[y];e;e=pre.E[e].next){
          int x=pre.E[e].to;if(!dfn[x])continue;
          if(dfn[x]<dfn[y])tmp=min(tmp,dfn[x]);
          else uset.find(x),tmp=min(tmp,dfn[semi[uset.Mi[x]]]);
        }
        semi[y]=rev[tmp];uset.fa[y]=f[y];
        dom.link(semi[y],y);
        
        y=rev[i-1];
        for(int e=dom.head[y];e;e=dom.E[e].next){
          int x=dom.E[e].to;uset.find(x);
          if(semi[uset.Mi[x]]==y)idom[x]=y;
          else idom[x]=uset.Mi[x];
        }
      }
    
      for(int i=2;i<=n;++i){
        int x=rev[i];
        if(idom[x]!=semi[x])
          idom[x]=idom[idom[x]];
      }
      
      dom.clear();
      for(int i=1;i<n;++i)
        dom.link(idom[i],i);
      dfs(n,0);
      for(int i=1;i<=n;++i){
        printf("%d",Ans[i]),Ans[i]=0;
        i==n?printf("
    "):printf(" ");
      }
    }
    
    int main(){
      while(~scanf("%d%d",&n,&m)){
        for(int i=1;i<=m;++i){
          int u=gi(),v=gi();
          nxt.link(u,v);
          pre.link(v,u);
        }
        tarjan(n);
        uset.init();
        calc();
        pre.clear();nxt.clear();dom.clear();
        for(int i=1;i<=n;++i)
          dfn[i]=rev[i]=semi[i]=idom[i]=f[i]=0;
        n=0;m=0;clo=0;
      }
      fclose(stdin);fclose(stdout);
      return 0;
    }
    HDU4694

     

    Codechef GRAPHCNT

    大意:问有多少个点对(x,y),满足存在路径(1=>x)和(1=>y)且两条路径公共点只有1。

    就是支配树上lca为1的点的点对嘛……

    #include <iostream>
    #include <cstdio>
    #include <cstdlib>
    #include <algorithm>
    #include <cstring>
    #include <vector>
    #include <cmath>
    #include <map>
    #include <set>
    #define LL long long
    #define FILE "graphcnt"
    using namespace std;
    
    const int N = 100010;
    const int M = 500010;
    int n,m,fa[N],dfn[N],rev[N],clo,semi[N],idom[N],size[N];
    
    inline int gi(){
      int x=0,res=1;char ch=getchar();
      while(ch>'9' || ch<'0')res^=ch=='-',ch=getchar();
      while(ch>='0'&&ch<='9')x=x*10+ch-48,ch=getchar();
      return res?x:-x;
    }
    
    struct Node{int to,next;};
    struct Graph{
      Node E[M];int head[N],tot;
      inline void clr(){
        for(int i=tot=0;i<=n;++i)head[i]=0;
      }
      inline void link(int u,int v){
        E[++tot]=(Node){v,head[u]};
        head[u]=tot;
      }
    }pre,nxt,dom;
    
    struct Union_Merge_Set{
      int fa[N],Mi[N];
      inline void init(){
        for(int i=1;i<=n;++i)
          fa[i]=Mi[i]=semi[i]=i;
      }
      inline int find(int x){
        if(fa[x]==x)return x;
        int fx=fa[x],y=find(fa[x]);
        if(dfn[semi[Mi[fx]]]<dfn[semi[Mi[x]]])Mi[x]=Mi[fx];
        return fa[x]=y;
      }
    }uset;
    
    inline void tarjan(int x){
      dfn[x]=++clo;rev[clo]=x;
      for(int e=nxt.head[x];e;e=nxt.E[e].next)
        if(!dfn[nxt.E[e].to])
          fa[nxt.E[e].to]=x,tarjan(nxt.E[e].to);
    }
    
    inline void build(){
      for(int i=n;i>=2;--i){
        int y=rev[i],tmp=n;if(!y)continue;
        for(int e=pre.head[y];e;e=pre.E[e].next){
          int x=pre.E[e].to;if(!dfn[x])continue;
          if(dfn[x]<dfn[y])tmp=min(tmp,dfn[x]);
          else uset.find(x),tmp=min(tmp,dfn[semi[uset.Mi[x]]]);
        }
        semi[y]=rev[tmp];uset.fa[y]=fa[y];
        dom.link(semi[y],y);
    
        y=rev[i-1];if(!y)continue;
        for(int e=dom.head[y];e;e=dom.E[e].next){
          int x=dom.E[e].to;uset.find(x);
          if(semi[uset.Mi[x]]==y)idom[x]=y;
          else idom[x]=uset.Mi[x];
        }
      }
      for(int i=2;i<=n;++i){
        int x=rev[i];
        if(idom[x]!=semi[x])
          idom[x]=idom[idom[x]];
      }
      dom.clr();
      
      for(int i=2;i<=n;++i)
        dom.link(idom[rev[i]],rev[i]);
    }
    
    inline void dfs(int x){
      size[x]=1;
      for(int e=dom.head[x];e;e=dom.E[e].next){
        int y=dom.E[e].to;if(size[y])continue;
        dfs(y);size[x]+=size[y];
      }
    }
    
    inline LL calc(LL Ans=0,LL sum=0){
      for(int e=dom.head[1];e;e=dom.E[e].next){
        int y=dom.E[e].to;
        Ans+=sum*size[y];
        sum+=size[y];
      }
      return Ans+size[1]-1;
    }
    
    int main(){
      n=gi();m=gi();
      for(int i=1;i<=m;++i){
        int u=gi(),v=gi();
        nxt.link(u,v);
        pre.link(v,u);
      }
      tarjan(1);
      uset.init();
      build();
      dfs(1);
      printf("%lld",calc());
      return 0;
    }
    Codechef GRAPHCNT

     

    最后来波总结

    算法时间复杂度O(nα(n)),空间复杂度O(n),但是常数比较大,虽然跑得还是很快。

    支配树本身的代码还是比较短的,细节有一点但都很正常,只要理解了绝对没有什么问题,就算没理解也没什么问题……

    这方面的题目目前比较少?小强和阿米巴?毕竟2014年才在Wc中普及……可能就快了吧,毕竟还是有一定实际意义和证明难度的。

    说起Wc又是另一回事了……

    怎么年年Wc扯支配树啊......

  • 相关阅读:
    Windows性能计数器应用
    Azure Oracle Linux VNC 配置
    Azure 配置管理系列 Oracle Linux (PART6)
    Azure 配置管理系列 Oracle Linux (PART5)
    Azure 配置管理系列 Oracle Linux (PART4)
    Azure 配置管理系列 Oracle Linux (PART3)
    Azure 配置管理系列 Oracle Linux (PART2)
    vagrant多节点配置
    docker基本操作
    LINUX开启允许对外访问的网络端口命令
  • 原文地址:https://www.cnblogs.com/fenghaoran/p/dominator_tree.html
Copyright © 2011-2022 走看看