求解強連通分量演算法之---Kosaraju演算法
本文提綱:
- 問題描述
- Kosaraju 演算法
問題描述:
什麼是強連通分量(StronglyConnected Component)(或者,被稱為強連通子圖,Strongly Connected Subgraph)?
首先需要明白的是,強連通分量只可能存在於有向圖中,無向圖中是不存在強連通分量的,當然,無向圖中也有對應物,被稱為連通分量(Connected Component),求解無向圖中的連通分量,根據具體要求,可以選擇使用並查集或者DFS。
看一張取自wiki的圖就明白什麼是強連通分量了:
以上用虛線圍繞的部分就是一個強連通分量,因此上圖中總共含有三個。
對於一個強連通分量中的任意一對頂點
比如上圖中由a,b,e這三個頂點構成的分量中,任意兩個頂點間都存在路徑可達。
順便也介紹一下有關“縮點”的概念:
由於強連通分量的特殊性,在一些實際應用中,會將每個強連通分量看成一個點,然後進行處理。這樣做主要是為了降低圖的複雜度,特別是在強連通分量規模大、數量多的情況中,利用“縮點”能大幅度降低圖的複雜度。
縮點後得到的圖,必定是DAG。用反正能夠很方便的進行證明:因為若圖中含有環路,即意味著至少有兩個點彼此可達,那麼按照強連通分量的定義,這兩個點應該屬於一個分量中,因而在縮點發生後,會被一個點所代表。由此推匯出矛盾。比如,對上圖進行縮點處理,最後的結果就是:
設(a,b,c) ->a',(f,g) ->b',(c,d,h) ->c'
因此最後的圖就可以表示為:
更加具體,更加嚴謹的表述,可以參考:
Kosaraju 演算法
首先摘錄一段wiki上的演算法過程描述:
- Let G be a directed graph and S be an empty stack.
- While S does not contain all vertices:
-
Choose an arbitrary vertex v not in S. Perform a depth-first search starting at v. Each time that depth-first search finishes expanding a vertex
u, push u onto S.
-
Choose an arbitrary vertex v not in S. Perform a depth-first search starting at v. Each time that depth-first search finishes expanding a vertex
u, push u onto S.
- Reverse the directions of all arcs to obtain the transpose graph.
-
While S is nonempty:
- Pop the top vertex v from S. Perform a depth-first search starting at v. The set of visited vertices will give the strongly connected component containing v; record this and remove all these vertices from the graph G and the stack S. Equivalently, breadth-first search (BFS) can be used instead of depth-first search.
仔細觀察步驟2-a,可以發現,這個過程和使用基於DFS的拓撲排序中新增頂點到最終結果棧中的過程幾乎一致。(請參考這裡)只不過這裡的圖不一定是DAG,而拓撲排序中的圖一定是DAG。
在這裡順便提一下在呼叫dfs的過程中,幾種新增頂點到集合的順序。一共有四種順序:
- Pre-Order,在遞迴呼叫dfs之前將當前頂點新增到queue中
- Reverse Pre-Order,在遞迴呼叫dfs之前將當前頂點新增到stack中
- Post-Order,在遞迴呼叫dfs之後將當前頂點新增到queue中
- Reverse Post-Order,在遞迴呼叫dfs之後將當前頂點新增到stack中
最後一種的用途最廣,至少目前看來是這樣,比如步驟2-a以及拓撲排序中,都是利用的Reverse Post-Order來獲取頂點集合。
對於DAG,利用Reverse Post-Order最終獲取的集合就代表對該圖進行拓撲排序的結果,但是對於非DAG而言,這樣處理之後得到的是一種“偽拓撲排序”結果,為什麼這樣說,因為最終的結果不一定滿足拓撲排序中嚴格的偏序定義,比如對文中第一幅圖進行“偽拓撲排序“後的結果為可能為(結果不唯一,具體可以參考拓撲排序那篇文章中的相關部分):
(a,b,e,c,d,h,g,f)
將結果和原圖對比,可以發現,結果不滿足偏序關係,因為存在迴向邊:
e->a,d->c,h->d,f->g
而這些迴向邊,就是構成強連通分量的關鍵。為了突出這些迴向邊,Kosaraju演算法的步驟三就是將圖進行轉置。轉置後,原來的迴向邊就都變成正向邊了:
a->e,c->d,d->h,g->f
對轉置後的圖按照上面”偽拓撲排序“中頂點出現的順序(a,e,b,c,d,h,g,f)呼叫DFS,即步驟4。
而每次呼叫DFS形成的一顆搜尋樹,就構成了原圖中的一個強連通分量。比如呼叫dfs(a),會呼叫dfs(e),緊接著後者有呼叫了dfs(b)。然後依次返回,因此a,b,e就構成了一個強連通分量。依次類推,c,d,h以及g,f也分別構成強連通分量。
回顧一下Kosaraju的主要步驟:
- 對G求解Reverse Post-Order,即上文中的”偽拓撲排序“
- 對G進行轉置得到GR
- 按照第一步得到的集合中頂點出現的順序,對GR呼叫DFS得到若干顆搜尋樹
- 每一顆搜尋樹就代表了一個強連通分量
坦率地說,這個演算法的想法很巧妙,為了突出迴向邊,而對圖進行轉置,然後對轉置的圖按照之前得到的頂點序列進行DFS呼叫。整個演算法的確能夠正確工作,但是總感覺怪怪的,確實,這個演算法不太好理解,儘管它的實現十分簡單直觀。
使用Java的實現程式碼:
public class KosarajuSCC {
private Digraph digraph;
private int V;
private boolean[] visited;
private int[] components;
private List<List<Integer>> sccs;
// record the current component id
private int current = 0;
// reverseTopo is not necessarily a topological order, it should be reverse
// post order instead
public KosarajuSCC(Digraph digraph, Iterable<Integer> reverseTopo) {
this.digraph = digraph;
V = digraph.getV();
visited = new boolean[V];
components = new int[V];
for (int v : reverseTopo) {
if (!visited[v]) {
dfs(v);
current++;
}
}
}
private void dfs(int v) {
visited[v] = true;
components[v] = current;
for (int w : digraph.adj(v)) {
if (!visited[w]) {
dfs(w);
}
}
}
public int[] getComponents() {
return components;
}
public List<List<Integer>> getSccs() {
sccs = new ArrayList<List<Integer>>();
for (int i = 0; i < current; i++) {
sccs.add(new ArrayList<Integer>());
}
for (int i = 0; i < V; i++) {
sccs.get(components[i]).add(i);
}
return sccs;
}
}
下面對這個演算法的正確性進行證明:(如果沒有興趣,可以直接略過:D)
證明的目標,就是最後一步 ---每一顆搜尋樹代表的就是一個強連通分量
證明:設在圖GR中,呼叫DFS(s)能夠到達頂點v,那麼頂點s和v是強連通的。
兩個頂點如果是強連通的,那麼彼此之間都有一條路徑可達,因為DFS(s)能夠達到頂點v,因此從s到v的路徑必然存在。現在關鍵就是需要證明在GR中從v到s也是存在一條路徑的,也就是要證明在G中存在s到v的一條路徑。
而之所以DFS(s)能夠在DFS(v)之前被呼叫,是因為在對G獲取ReversePost-Order序列時,s出現在v之前,這也就意味著,v是在s之前加入該序列的(因為該序列使用棧作為資料結構,先加入的反而會在序列的後面)。因此根據DFS呼叫的遞迴性質,DFS(v)應該在DFS(s)之前返回,而有兩種情形滿足該條件:
- DFS(v) START -> DFS(v) END -> DFS(s) START -> DFS(s) END
- DFS(s) START -> DFS(v) START -> DFS(v) END -> DFS(s) END
是因為而根據目前的已知條件,GR中存在一條s到v的路徑,即意味著G中存在一條v到s的路徑,而在第一種情形下,呼叫DFS(v)卻沒能在它返回前遞迴呼叫DFS(s),這是和G中存在v到s的路徑相矛盾的,因此不可取。故情形二為唯一符合邏輯的呼叫過程。而根據DFS(s) START ->DFS(v) START可以推匯出從s到v存在一條路徑。
所以從s到v以及v到s都有路徑可達,證明完畢。
複雜度分析:
根據上面總結的Kosaraju演算法關鍵步驟,不難得出,該演算法需要對圖進行兩次DFS,以及一次圖的轉置。所以複雜度為O(V+E)。