支配树(dominator tree) 学习笔记
学习背景
本来本蒟蒻都不知道有一个东西叫支配树……pkuwc前查某位的水表看见它的大名,甚感恐慌啊。不过好在pkuwc5道题(嗯?)都是概率期望计数,也不知是好还是不好,我在这些方面也只是不好不差……扯远了。
考挂之后也没什么心思干别的,想起支配树这个东西,于是打算学一下。
技能介绍(雾)
支配树是什么?不如直接讲支配树的性质,从性质分析它的定义。
先大概讲一下它是来求什么的。
问题:我们有一个有向图(可以有环),定下了一个节点为起点s。现在我们要求:从起点s出发,走向一个点p的所有路径中,必须要经过的点有哪些{xp}。
换言之,删掉{xp}中的任意一个点xpi以及它的入边出边,都会使s无法到达p。
我们有一种显然的O(nm)的方法:枚举+BFS。
现在我们学习构造图的支配树,它是一种复杂度更优秀的做法。
性质:
- 它是一棵树(这不废话),根节点是我们选定的起点s。
- 对于每个点i,它到根的链上的点集就是对于它的必经点集{xi}。
- 对于每个点i,它是它的支配树上的子树内的点的必经点。
所以对于上面的问题,把支配树抠出来就可以了。
算法原理
先来看一下两种比较简单的情况。不妨假设从s出发可以到达图的所有点,不失一般性。
树
显而易见的,树就是自己的支配树……
有向无环图(DAG)
DAG上的问题当然要靠拓扑序来搞啦啦啦!
我们利用拓扑序做。对于一个点,所有能到达它的点在支配树中的lca,就是它支配树中的父亲。
用倍增求lca可以做到O(nlogn)。
比如说 ZJOI2012 灾难
答案就是支配树上的size。
当时这道题好像也挺难……谁能想到新建树啊……
一般有向图
显然支配具有传递性。
先随便搞出一棵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在支配树上的父亲。
显然有下面的性质:
- 每个点的半支配点是唯一的。
- 一个点的半支配点必定是它在dfs树上的祖先,dfn[semi[x]]<dfn[x]。
- 半支配点不一定是x的关键点。
- semi[x]的深度不小于idom[x]的深度,即idom[x]在semi[x]的祖先链上。
- 设节点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]。
对黑字的一些理解:
下面的一切涉及大小的都是用dfn做比较的,不然太丑了……
第一行。
- 由性质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; }
具体实现
算法名叫: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; }
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; }
最后来波总结
算法时间复杂度O(nα(n)),空间复杂度O(n),但是常数比较大,虽然跑得还是很快。
支配树本身的代码还是比较短的,细节有一点但都很正常,只要理解了绝对没有什么问题,就算没理解也没什么问题……
这方面的题目目前比较少?小强和阿米巴?毕竟2014年才在Wc中普及……可能就快了吧,毕竟还是有一定实际意义和证明难度的。
说起Wc又是另一回事了……