本篇口胡寫給我自己這樣的老是證錯東西的口胡選手 以及那些想學支配樹,又不想啃論文原文的人…
大概會講的東西是求支配樹時需要用到的一些性質,以及構造支配樹的算法實現…
最后講一下把只有路徑壓縮的並查集卡到$O(m \log n)$上界的辦法作為小彩蛋…
1、基本介紹 支配樹 DominatorTree
對於一個流程圖(單源有向圖)上的每個點$w$,都存在點$d$滿足去掉$d$之后起點無法到達$w$,我們稱作$d$支配$w$,$d$是$w$的一個支配點。
支配$w$的點可以有多個,但是至少會有一個。顯然,對於起點以外的點,它們都有兩個平凡的支配點,一個是自己,一個是起點。
在支配$w$的點中,如果一個支配點$i \neq w$滿足$i$被$w$剩下的所有非平凡支配點支配,則這個$i$稱作$w$的最近支配點(immediate dominator),記作$idom(w)$。
定理1:我們把圖的起點稱作$r$,除$r$以外每個點均存在唯一的$idom$。
這個的證明很簡單:如果$a$支配$b$且$b$支配$c$,則$a$一定支配$c$,因為到達$c$的路徑都經過了$b$所以必須經過$a$;如果$b$支配$c$且$a$支配$c$,則$a$支配$b$(或者$b$支配$a$),否則存在從$r$到$b$再到$c$的路徑繞過$a$,與$a$支配$c$矛盾。這就意味着支配定義了點$w$的支配點集合上的一個全序關系,所以一定可以找到一個“最小”的元素使得所有元素都支配它。
於是,連上所有$r$以外的$idom(w) \to w$的邊,就能得到一棵樹,其中每個點支配它子樹中的所有點,它就是支配樹。
支配樹有很多食用…哦不…是實際用途。比如它展示了一個信息傳遞網絡的關鍵點,如果一個點支配了很多點,那么這個點的傳遞效率和穩定性要求都會很高。比如Java的內存分析工具(Memory Analyzer Tool)里面就可以查看對象間引用關系的支配樹…很多分析上支配樹都是一個重要的參考。
為了能夠求出支配樹,我們下面來介紹一下需要用到的基本性質。
2、支配樹相關性質
首先,我們會使用一棵DFS樹來幫助我們計算。從起點出發進行DFS就可以得到一棵DFS樹。
觀察上面這幅圖,我們可以注意到原圖中的邊被分為了幾類。在DFS樹上出現的邊稱作樹邊,剩下的邊稱為非樹邊。非樹邊也可以分為幾類,從祖先指向后代(前向邊),從后代指向祖先(后向邊),從一棵子樹內指向另一棵子樹內(橫叉邊)。樹邊是我們非常熟悉的,所以着重考慮一下非樹邊。
我們按照DFS到的先后順序給點從小到大編號(在下面的內容中我們通過這個比較兩個節點),那么前向邊總是由編號小的指向編號大的,后向邊總是由大指向小,橫叉邊也總是由大指向小。現在在DFS樹上我們要證明一些重要的引理:
引理1(路徑引理):
如果兩個點$v,w$滿足$v \leq w$,那么任意$v$到$w$的路徑經過$v,w$的公共祖先。(注意這里不是說LCA)
證明:
如果$v,w$其中一個是另一個的祖先顯然成立。否則刪掉起點到LCA路徑上的所有點(這些點是$v,w$的公共祖先),那么$v$和$w$在兩棵子樹內,並且因為公共祖先被刪去,無法通過后向邊到達子樹外面,前向邊也無法跨越子樹,而橫叉邊只能從大到小,所以從$v$出發不能離開這顆子樹到達$w$。所以如果本來$v$能夠到達$w$,就說明這些路徑必須經過$v,w$的公共祖先。
在繼續之前,我們先約定一些記號:
$V$代表圖的點集,$E$代表圖的邊集。
$a \to b$代表從點$a$直接經過一條邊到達點$b$,
$a \leadsto b$代表從點$a$經過某條路徑到達點$b$,
$a \dot \to b$代表從點$a$經過DFS樹上的樹邊到達點$b$($a$是$b$在DFS樹上的祖先),
$a \overset{+}{\to} b$代表$a \dot \to b$且$a \neq b$。
定義 半支配點(semi-dominator):
對於$w \neq r$,它的半支配點定義為$sdom(w)=\min\{ v | \exists (v_0,v_1,\cdots,v_{k-1},v_k), v_0 = v, v_k = w, \forall 1 \leq i \leq k-1, v_i>w \}$
對於這個定義的理解其實就是從$v$出發,繞過$w$之前的所有點到達$w$。(只能以它之后的點作為落腳點)
注意這只是個輔助定義,並不是真正的支配點。甚至在只保留$w$和$w$以前的點時它都不一定是支配點。例子:$V = \{1,2,3,4\}, E = \{(1,2),(2,3),(3,4),(1,3),(2,4)\}, r = 1, sdom(4) = 2$,但是$2$不支配$4$。不過它代表了有潛力成為支配點的點,在后面我們可以看到,所有的$idom$都來自自己或者另一個點的$sdom$。
引理2
對於任意$w \neq r$,有$idom(w) \overset{+}{\to} w$。
證明很顯然,如果不是這樣的話就可以直接通過樹邊不經過$idom(w)$就到達$w$了,與$idom$定義矛盾。
引理3
對於任意$w \neq r$,有$sdom(w) \overset{+}{\to} w$。
證明:
對於$w$在DFS樹上的父親$fa_w$,$fa_w \to w$這條路徑只有兩個點,所以滿足$sdom$定義中的條件,於是它是$sdom(w)$的一個候選。所以$sdom(w) \leq fa_w$。在這里我們就可以使用路徑引理證明$sdom(w)$不可能在另一棵子樹,因為如果是那樣的話就會經過$sdom(w)$和$w$的一個公共祖先,公共祖先的編號一定小於$w$,所以不可行。於是$sdom(w)$就是$w$的真祖先。
引理4
對於任意$w \neq r$,有$idom(w) \dot \to sdom(w)$。
證明:
如果不是這樣的話,按照$sdom$的定義,就會有一條路徑是$r \dot \to sdom(w) \leadsto w$不經過$idom(w)$了,與$idom$定義矛盾。
引理5
對於滿足$v \dot \to w$的點$v,w$,$v \dot \to idom(w)$或$idom(w) \dot \to idom(v)$。
(不嚴謹地說就是$idom(w)$到$w$的路徑不相交或者被完全包含,其實$idom(w)$這個位置是可能相交的)
證明:
如果不是這樣的話,就是$idom(v) \overset{+}{\to} idom(w) \overset{+}{\to} v \overset{+}{\to} w$,那么存在路徑$r \dot \to idom(v) \leadsto v \overset{+}{\to}w$不經過$idom(w)$到達了$w$(因為$idom(w)$是$idom(v)$的真后代,一定不支配$v$,所以存在繞過$idom(w)$到達$v$的路徑),矛盾。
上面這5條引理都比較簡單,不過是非常重要的性質。接下來我們要證明幾個定理,它們揭示了$idom$與$sdom$的關系。證明可能會比上面的復雜一點。
定理2
對於任意$w \neq r$,如果所有滿足$sdom(w) \overset{+}{\to} u \dot \to w$的$u$也滿足$sdom(u) \geq sdom(w)$,那么$idom(w) = sdom(w)$。
$$ sdom(w) \dot \to sdom(u) \overset{+}{\to} u \dot \to w $$
證明:
由上面的引理4知道$idom(w) \dot \to sdom(w)$,所以只要證明$sdom(w)$支配$w$就可以保證是最近支配點了。對任意$r$到$w$的路徑,取上面最后一個編號小於$sdom(w)$的$x$(如果$sdom$就是$r$的話顯然定理成立),它必然有個后繼$y$滿足$sdom(w) \dot \to y \dot \to w$(否則$x$會變成$sdom(w)$),我們取最小的那個$y$。同時,如果$y$不是$sdom(w)$,根據條件,$sdom(y) \geq sdom(w)$,所以$x$不可能是$sdom(y)$,這就意味着$x$到$y$的路徑上一定有一個$v$滿足$x \overset{+}{\to} v \overset{+}{\to} y$,因為$x$是小於$sdom(w)$的最后一個,所以$v$也滿足$sdom(w) \dot \to v \dot \to w$,但是我們取的$y$已經是最小的一個了,矛盾。於是$y$只能是$sdom(w)$,那么我們就證明了對於任意路徑都要經過$sdom(w)$,所以$sdom(w)$就是$idom(w)$。
定理3
對於任意$w \neq r$,令$u$為所有滿足$sdom(w) \overset{+}{\to} u \dot \to w$的$u$中$sdom(u)$最小的一個,那么$sdom(u) \leq sdom(w) \Rightarrow idom(w) = idom(u)$。
$$ sdom(u) \dot \to sdom(w) \overset{+}{\to} u \dot \to w $$
證明:
由引理5,有$idom(w) \dot \to idom(u)$或$u \dot \to idom(w)$,由引理4排除后面這種。所以只要證明$idom(u)$支配$w$即可。類似定理2的證明,我們取任意$r$到$w$路徑上最后一個小於$idom(u)$的$x$(如果$idom(u)$是$r$的話顯然定理成立),路徑上必然有個后繼$y$滿足$idom(u) \dot \to y \dot \to w$(否則$x$會變成$sdom(w)$),我們取最小的一個$y$。類似上面的證明,我們知道$x$到$y$的路徑上不能有點$v$滿足$idom(u) \dot \to v \overset{+}{\to} y$,於是$x$成為$sdom(y)$的候選,所以$sdom(y) \leq x$。那么根據條件我們也知道了$y$不能是$sdom(w)$的真后代,於是$y$滿足$idom(u) \dot \to y \dot \to sdom(w)$。但是我們注意到因為$sdom(y) \leq x$,存在一條路徑$r \dot \to sdom(y) \leadsto y \dot \to u$,如果$y$不是$idom(u)$的話這就是一條繞過$idom(u)$的到$u$的路徑,矛盾,所以$y$必定是$idom(u)$。所以任意到$w$的路徑都經過$idom(u)$,所以$idom(w)=idom(u)$ 。
幸苦地完成了上面兩個定理的證明,我們就能夠通過$sdom$求出$idom$了:
推論1
對於$w \neq r$,令$u$為所有滿足$sdom(w) \overset{+}{\to} u \dot \to w$的$u$中$sdom(u)$最小的一個,有
$$ idom(w) = \left \{ \begin{aligned}& sdom(w)&(sdom(u)=sdom(w))&\\ &idom(u)&(sdom(u)<sdom(w))&\end{aligned} \right .$$
通過定理2和定理3可以直接得到。這里一定有$sdom(u) \leq sdom(w)$,因為$w$也是$u$的候選。
接下來我們的問題是,直接通過定義計算$sdom$很低效,我們需要更加高效的方法,所以我們證明下面這個定理:
定理4
對於任意$w \neq r$,$sdom(w) = min(\{v | (v, w) \in E , v < w \} \cup \{sdom(u) | u > w , \exists (v, w) \in E , u \dot \to v\} )$
證明:
令等號右側為$x$,顯然右側的點集中都存在路徑繞過$w$之前的點,所以$sdom(w) \leq x$。然后我們考慮$sdom(w)$到$w$的繞過$w$之前的點的路徑,如果只有一條邊,那么必定滿足$(sdom(w),w) \in E$且$sdom(w)<w$,所以此時$x \leq sdom(w)$;如果多於一條邊,令路徑上$w$的上一個點為$last$,我們取路徑上除兩端外滿足$p \dot \to last$的最小的$p$(一定能取得這樣的$p$,因為$last$是$p$的候選)。因為這個$p$是最小的,所以$sdom(w)$到$p$的路徑必定繞過了$p$之前的所有點,於是$sdom(w)$是$sdom(p)$的候選,所以$sdom(p) \leq sdom(w)$。同時,$sdom(p)$還滿足右側的條件($p$在繞過$w$之前的點的路徑上,於是$p>w$,並且$p\dot \to last$,同時$last$直接連到了$w$),所以$sdom(p)$是$x$的候選,$x \leq sdom(p)$。所以$x \leq sdom(p) \leq sdom(w)$,$x \leq sdom(w)$。綜上,$sdom(w) \leq x$且$x \leq sdom(w)$,所以$x=sdom(w)$。
好啦,最困難的步驟已經完成了,我們得到了$sdom$的一個替代定義,而且這個定義里面的形式要簡單得多。這種基本的樹上操作我們是非常熟悉的,所以沒有什么好擔心的了。接下來就可以給出我們需要的算法了。
3、Lengauer-Tarjan算法
算法流程:
1、初始化、跑一遍DFS得到DFS樹和標號
2、按標號從大到小求出$sdom$(利用定理4)
3、通過推論1求出所有能確定的$idom$,剩下的點記錄下和哪個點的$idom$是相同的
4、按照標號從小到大再跑一次,得到所有點的$idom$
很簡單對不對~有了理論基礎后算法就很顯然了。
具體實現:
大致要維護的東西:
$vertex(x)$ 標號為$x$的點$u$
$pred(u)$ 有邊直接連到$u$的點集
$parent(u)$ $u$在DFS樹上的父親$fa_u$
$bucket(u)$ $sdom$為點$u$的點集
以及$idom$和$sdom$數組
第1步沒什么特別的,規規矩矩地DFS一次即可,同時初始化$sdom$為自己(這是為了實現方便)。
第2、3步可以一起做。通過一個輔助數據結構維護一個森林,支持加入一條邊($link(u,v)$)和查詢點到根路徑上的點的$sdom$的最小值對應的點($eval(u)$)。那么我們求每個點的$sdom$只需要對它的所有直接前驅$eval$一次,求得前驅中的$sdom$最小值即可。因為定理4中的第一類點編號比它小,它們還沒有處理過,所以自己就是根,$eval$就能取得它們的值;對於第二類點,$eval$查詢的就是滿足$u \dot \to v$的$u$的$sdom(u)$的最小值。所以這么做和定理4是一致的。
然后把該點加入它的$sdom$的$bucket$里,連上它與父親的邊。現在它父親到它的這棵子樹中已經處理完了,所以可以對父親的$bucket$里的每個點求一次$sdom$並且清空$bucket$。對於$bucket$里的每個點$v$,求出$eval(v)$,此時$parent(w) \overset{+}{\to} eval(v) \dot \to v$,於是直接按照推論1,如果$sdom(eval(v))=sdom(v)$,則$idom(v)=sdom(v)=parent(w)$;否則可以記下$idom(v)=idom(eval(v))$,實現時我們可以寫成$idom(v)=eval(v)$,留到第4步處理。
最后從小到大掃一遍完成第4步,對於每個$u$,如果$idom(u)=sdom(u)$的話,就已經是第3步求出的正確的$idom$了,否則就證明這是第3步留下的待處理點,令$idom(u)=idom(idom(u))$即可。
對於這個輔助數據結構,我們可以選擇並查集。不過因為我們需要查詢到根路徑上的信息,所以不方便寫按秩合並,但是我們仍然可以路徑壓縮,壓縮時保留路徑上的最值就可以了,所以並查集操作的復雜度是$O(\log n)$。這樣做的話,最終的復雜度是$O(n \log n)$。(各種常見方法優化的並查集只要沒有按秩合並就是做不到$\alpha$的復雜度的,最下面我會提到如何卡路徑壓縮)
原論文還提到了一個比較奧妙的實現方法,能夠把這個並查集優化到$\alpha$的復雜度,不過看上去比較迷,我覺得我會寫錯,所以就先放着了,如果有興趣的話可以找原論文A Fast Algorithm for Finding Dominators in a Flowgraph,里面的參考文獻14是Tarjan的另一篇東西Applications of Path Compression on Balanced Trees,原論文說用的是這里面的方法…等什么時候無聊想要真正地學習並查集的各種東西的時候再看吧…(我又挖了個大坑)
代碼實現
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 #include <bits/stdc++.h> using namespace std; inline int read() { int s = 0; char c; while((c=getchar())<'0'||c>'9'); do{s=s*10+c-'0';}while((c=getchar())>='0'&&c<='9'); return s; } const int N = 200010; struct eg{ int dt,nx; }e[N]; int n,m,tim,tot; int h[N],iw[N],li[N],fa[N],sdom[N],idom[N]; int fo[N],vo[N]; vector<int> pre[N],bkt[N]; int findf(int p) { if(fo[p]==p) return p; int r = findf(fo[p]); if(sdom[vo[fo[p]]]<sdom[vo[p]]) vo[p] = vo[fo[p]]; return fo[p] = r; } inline int eval(int p) { findf(p); return vo[p]; } void dfs(int p) { li[iw[p]=++tim] = p, sdom[p] = iw[p]; for(int pt=h[p];pt;pt=e[pt].nx) if(!iw[e[pt].dt]) dfs(e[pt].dt), fa[e[pt].dt] = p; } void work() { int i,p; dfs(1); for(i=tim;i>=2;i--) { p = li[i]; for(int k : pre[p]) if(iw[k]) sdom[p] = min(sdom[p],sdom[eval(k)]); bkt[li[sdom[p]]].push_back(p); int fp = fa[p]; fo[p] = fa[p]; for(int v : bkt[fp]) { int u = eval(v); idom[v] = sdom[u]==sdom[v]?fp:u; } bkt[fp].clear(); } for(i=2;i<=tim;i++) p = li[i], idom[p] = idom[p]==li[sdom[p]]?idom[p]:idom[idom[p]]; for(i=2;i<=tim;i++) p = li[i], sdom[p] = li[sdom[p]]; } inline void link(int a,int b) { e[++tot].dt = b, e[tot].nx = h[a], h[a] = tot; pre[b].push_back(a); } int main() { #ifndef ONLINE_JUDGE freopen("in.txt","r",stdin); #endif int i; n = read(), m = read(); tim = tot = 0; for(i=1;i<=n;i++) h[i] = iw[i] = 0, fo[i] = vo[i] = i, pre[i].clear(), bkt[i].clear(); for(i=1;i<=m;i++){ int a = read(); link(a,read()); } work(); return 0; }
我的變量名都很迷…不要在意…(它們可是經過了長時間的結合中文+英文+象形+腦洞的演變得出的結果)
稍微需要注意一下的就是實現時點的真實編號和DFS序中的編號的區別,DFS序的編號是用來比較的那個。以及盡量要保持一致性(要么都用真實編號,要么都用DFS序編號),否則很容易寫錯…我的這段代碼里$idom$用的是真實編號,$sdom$用的是DFS序編號,最后再跑一次把$sdom$轉成真實編號的。
4、歡快的彩蛋 卡並查集!
是不是聽到周圍有人說:“我的並查集只寫了路徑壓縮,它是單次操作$\alpha$的”。這時你要堅定你的信念,你要相信這是$O(\log n)$的。如果他告訴你這個卡不了的話…你或許會覺得確實很難卡…我也覺得很難卡…但是Tarjan總知道怎么卡。
現在確認一下純路徑壓縮並查集的實現方法:每次基本操作$find(v)$后都把$v$到根路徑上的所有點直接接在根的下面,每次合並操作對需要合並的兩個點執行$find$找到它們的根。
看起來挺優的。(其實真的挺優的,只是沒有$\alpha$那么優)
Tarjan的卡法基於一種特殊定義的二項樹(和一般的二項樹的定義不同)。
定義這種特殊的二項樹$T_k$為一類多叉樹,其中$T_1,T_2,\cdots,T_j$都是一個單獨的點,對於$T_k, k>j$,$T_k$就是$T_{k-1}$再接上一個$T_{k-j}$作為它的兒子。
就像這樣。這種定義有一個有趣的特性,如果我們把它繼續展開,可以得到各種有趣的結果。比如我們把上面圖中的$T_{k-j}$繼續展開,就會變成$T_{k-j-1}$接着$T_{k-2j}$,以此類推可以展開出一串。而如果對$T_{k-1}$繼續展開,父節點就會變成$T_{k-2}$,子節點多出一個$T_{k-j-1}$,以此類推可以展開成一層樹。下面的圖展示了展開$T_k$的不同方式。
讓我們好好考慮一下這意味着什么。從圖4到圖5…除了這些樹的編號沒有對應上以外,會不會有一種感覺,圖5像是圖4路徑壓縮后的結果。
圖4的展開方式中編號的間隔都是$j$,圖5的展開方式中間隔都是$1$…那么如果我們用圖5的方式展開出$j$棵子樹,再按圖4展開會怎么樣呢?(假設$j$整除$k$)
變成了這個樣子,就確實和路徑壓縮扯上關系了。如果在最頂上再加一個點,然后$j$次訪問底層的$T_1,T_2,\cdots,T_j$,就可以把樹壓成圖5的樣子了,不過會多一個單點的兒子出來,因為圖6中其實有兩個$T_j$(因為圖4展開到最后一層沒有了$-1$,所以會和上一層出現一次重復)。這么一來,我們又可以做一次這一系列操作了,非常神奇!(原論文里把這個叫做self-reproduction)至於$T_k$的實際點數,通過歸納法可以得到點數不超過$(j+1)^{\frac{k}{j}-1}$。(我們只對能被$j$整除的$k$進行計算,每次$j$次展開父節點進行歸納)
有了這個我們就有信心卡純路徑壓縮並查集了。令$m$代表詢問操作數,$n$代表合並操作數,不妨設$m \geq n$,我們取$j=\left \lfloor \frac{m}{n} \right \rfloor, i=\left \lfloor \log_{j+1}\frac{n}{2} \right \rfloor +1, k = ij$。那么$T_k$的大小不超過$(j+1)^{i-1}$即$\frac{n}{2}$。接下來我們做$\frac{n}{2}$組操作,每組在最頂上加入一個點,然后對底層的$j$個節點逐一查詢,每次查詢的路徑長度都是$i+1$。同時總共的查詢次數還是不超過$m$。於是總共的復雜度是$\frac{n}{2}j(i+1)=\Omega(m \log_{1+m/n} n)$。
Boom~爆炸了,所以它確實是$\log$級的。
彩蛋到這里就結束啦…如果想知道更多並查集優化方法怎么卡,可以去看這一部分參考的原論文Worst-Case Analysis of Set Union Algorithms,里面還附帶了一個表,有寫各種並查集實現不帶按秩合並和帶按秩合並的復雜度,嗯,卡並查集還是挺有趣的(只是一般人想不到呀…Tarjan太強辣)…
(題外話:這次我畫了好多圖,感覺自己好良心呀w 其實都是對着論文上的例子畫的)