支配樹(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。
當時這道題好像也挺難……誰能想到新建樹啊……
一般有向圖
注:下面的一切涉及大小的都是用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在支配樹上的父親。
顯然有下面的性質:
-
- 每個點的半支配點是唯一的。
- 一個點的半支配點必定是它在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]。
對黑字的一些理解:
第一行。
- 由性質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又是另一回事了……
怎么年年Wc扯支配樹啊......
