支配树学习笔记

支配树(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("\n");
  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("\n"):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扯支配树啊......

posted @ 2018-02-03 17:03  Fenghr  阅读(11486)  评论(5编辑  收藏  举报