這里主要談及強連通分量(以下簡稱SCC,strongly connected component)三種常見的求法(以下涉及的圖均為有向圖),即Kosaraju、Tarjan和Gabow。三種算法背后的基礎思想都是DFS,只是它們通過DFS獲得了不同的信息。各位大哥大姐繼續往下讀之前,最好對DFS相關的概念和性質比較熟悉,例如,什么叫做<a title="DFS" href="http://en.wikipedia.org/wiki/Depth-firstsearch" target="blank">反向邊、交叉邊</a>。
Kosaraju
我就不八卦這大哥了。Kosaraju的方法也就是導論第二版中文22章中講的方法, 即廣為人知的兩遍DFS。Kosaraju算法說白了就是先對原圖來一遍DFS, 再把所有邊的方向都倒過來,按照剛才DFS求出的結點完成時間的逆序,再來一遍DFS。
那么為啥可以這么做呢?
這就需要一個性質來幫忙,即當A、B為兩個SCC,且存在有向邊(a, b),其中a∈A,b∈B, 那么必然有:A的完成時間晚於B(一個SCC的完成時間表示該SCC中所有結點完成時間最晚的一個)。
可以簡單證明一下:
如果我們在第一遍DFS的時候,A先於B被訪問,且A中的第一個被訪問到的結點是x, 那A和B中所有的結點顯然都能在x之后被訪問到。於是x的完成時間要晚於B中任何一個結點, 從而A的完成時間晚於B。
如果B先於A被訪問,由於A和B是不同的兩個SCC,而且只有A到B的邊,於是B就不能到達A。 那么當B中的結點被訪問完之后,A中的點仍然處於為訪問狀態,自然A的完成時間也就晚於B了。
所以在第二遍DFS時,第一次取到的那個完成時間最晚的結點u, 它所在的SCC在轉置圖中就不能有指向外的邊。於是對轉置圖的第二遍DFS, 從u開始,便能輕易走遍所有處於同一SCC的結點。后續的遍歷步驟也就類似了。
時間復雜度自然是算在兩次DFS頭上,O(V+E)。
Tarjan
Tarjan貌似跟Hopcroft都是Cornell的大神。總的來說, Tarjan算法基於一個觀察,即:同處於一個SCC中的結點必然構成DFS樹的一棵子樹。 我們要找SCC,就得找到它在DFS樹上的根。
那么怎么找呢?
考慮一下,如果DFS訪問到了某個結點u,又順着u來到了結點v, 但從v發出了一條反向邊,指向了u的前驅w,那根據DFS的性質, u->v->w->u構成了一個環。這一堆東西必然處於同一個SCC。 所以某個要找到SCC子樹的根,就得找那個在DFS樹中最早被發現的結點,且這個結點要與它的一堆后繼結點形成環。
這時候DFS的特性就派上用場了。最早發現的結點可以通過記錄發現時間來實現,而反向邊的判斷可以通過結點顏色,即訪問狀態來實現。 定義一個結點的low值為:從該節點的子樹結點可達的,尚未求出屬於哪個SCC的結點的最早訪問時間。 由於SCC構成子樹,所以求沒求出某個結點所在的SCC用棧來刻畫就可以了: 每次訪問到一個結點u,記錄發現時間visit,並將它推到棧里去。 如果從u可達的結點v沒訪問過,那么訪問v,用v的low值更新u; 否則,如果v已訪問過,那就看看它在不在棧中。如果在,說明還沒確定v到底屬於哪個SCC, 這時(u, v)就是一條反向邊了,根據v的visit值,更新u的low值即可。 最后回到u結點時,如果u的low值和visit相等了,顯然u就是我們要找的根節點了。 從棧里把u和其上所有結點彈出來,這一堆東西就在一個SCC里了。上偽代碼:
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 |
// n: number of nodes in the graph
// visit[i]: discovery time of node i
// low[i]: low-value of node i
// time: the time stamp
// S: the stack
time <- 1
FIND_SCC(G):
for i<-1 to n:
if visit[i] is not defined:
TARJAN(i)
TARJAN(u):
visit[u] <- time
low[u] <- time
time <- time + 1
push(S, u)
for each edge (u, i) in the graph:
if visit[i] is not defined:
TARJAN(i)
low[u] <- min{ low[u], low[i] }
else if i is in S:
low[u] <- min{ low[u], visit[i] }
// things popped here are in the same SCC
if visit[u] == low[u]:
pop all node above u on stack including u
|
每個結點入棧一次出棧一次,每條邊訪問一次,O(V+E)。
Gabow
Gabow與Tarjan的思路是一致的。但Gabow使用了另一個棧來找出SCC子樹的根。 Gabow使用的棧S與Tarjan一樣,保存尚未決定屬於哪個SCC的結點; 棧P保持如下性質:棧頂結點始終具有最小的visit值, 即保持棧頂元素的visit值小於等於當前發現的反向邊指向的祖先結點的visit值。
棧S和P都隨着DFS的進行增長。若當前正在訪問結點u,從u可達點v, 先將u壓入兩個棧中。這一步驟相當於Tarjan中初始化一個結點的low值為當前visit值。 如果v沒有訪問過,則訪問v;否則判斷v是否在S棧中。 如果在,那么(u, v)為反向邊,此時從P棧頂彈出那些晚於v被發現的結點。為啥? 因為此時v是u的后繼結點,我們得找出以u為根的子樹結點能到達的最早訪問的結點, 類似於Tarjan算法中對low值的更新。
再一次回到u時,若P棧棧頂的元素就是u,表明u就是SCC子樹的根。 與Tarjan類似,從S棧中彈出元素即找到了一個SCC,代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
GABOW(u):
visit[u] <- time
time <- time + 1
push(S, u)
push(P, u)
for each edge (u, i) in the graph:
if visit[i] is not defined:
GABOW(i)
else if i is in S:
repeat popping nodes on P until visit[top(P)] <= visit[i]
// things popped here are in the same SCC
if top(P) == u:
pop all node above u on S including u
pop u from P
|
Gabow時間復雜度也為O(V+E),常數因子的差別各位大神請自行分析。
