• 支配树学习笔记


    支配树(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扯支配树啊......

  • 相关阅读:
    左偏树——可以标记合并的堆
    主席树——多棵线段树的集合
    [中山市选2011]完全平方数 ——莫比乌斯函数
    决策单调性优化dp
    [NOI2015]寿司晚宴——状压dp
    【[国家集训队]等差子序列】
    线性基——数集压缩自动机
    Java实现 蓝桥杯VIP 算法训练 筛选号码
    BSGS&EXBSGS 大手拉小手,大步小步走
    CRT&EXCRT 中国剩余定理及其扩展
  • 原文地址:https://www.cnblogs.com/fenghaoran/p/dominator_tree.html
Copyright © 2020-2023  润新知