Kosaraju
算法一看這個名字很奇怪就能夠猜到它也是一個根據人名起的算法,它的發明人是S. Rao Kosaraju
,這是一個在圖論當中很是著名的算法,能夠用來拆分有向圖當中的強連通份量。python
這裏有兩個關鍵詞,一個是有向圖,另一個是強連通份量。有向圖是它的使用範圍,咱們只能使用在有向圖當中。對於無向圖其實也存在強連通份量這個概念,但因爲無向圖的連通性很是強,只須要用一個集合維護就能夠知道連通的狀況,因此也沒有必要引入一些算法。算法
有向圖咱們都瞭解,那麼什麼叫作強連通份量呢?強連通份量的英文是strongly connected components。這是一個很直白的翻譯,要理解它咱們首先須要理解強連通的概念。在有向圖當中,若是兩個點之間彼此存在一條路徑相連,那麼咱們稱這兩個點強連通。那麼推廣一下,若是一張圖當中的一個部分中的每兩個點都連通,那麼這個部分就稱爲強連通份量。app
強連通份量通常是一張完整的圖的一個部分,好比下面這張圖當中的{1, 2, 3, 4}節點就能夠被當作是一個強連通份量。測試
其實求解強連通份量的算法並不止一種,除了Kosaraju以外還有大名鼎鼎的Tarjan算法能夠用來求解。但相比Tarjan算法,Kosaraju算法更加直觀,更加容易理解。ui
Kosaraju算法的原理很是簡單,簡單到只有三個步驟:翻譯
怎麼樣,是否是很簡單?code
下面咱們來詳細闡述一下細節,首前後序遍歷和維護出棧順序是一碼事。也就是在遞歸的過程中當咱們遍歷完了u這個節點全部連通的點以後,再把u加入序列。其實也就是u在遞歸出棧的時候纔會被加入序列,那麼序列當中存儲的也就是每一個點的出棧順序。component
這裏我用一小段代碼(python)演示一下,看完也就明白了。排序
popped = [] # 存儲出棧節點 def dfs(u): for v in Graph[u]: dfs(v) popped.append(u)
咱們在訪問完了全部的v以後再把u加入序列,這也就是後序遍歷,和二叉樹的後序遍歷是相似的。遞歸
反向圖也很好理解,因爲咱們求解的範圍是有向圖,若是原圖當中存在一條邊從u指向v,那麼反向圖當中就會有一條邊從v指向u。也就是把全部的邊都調轉反向。
咱們用上面的圖舉個例子,對於原圖來講,它的出棧順序咱們用紅色筆標出。
也就是[6, 4, 2, 5, 3, 1],咱們按照出棧順序從大到小排序,也就是將它反序一下,獲得[1, 3, 5, 2, 4, 6]。1是第一個,也就是最後一個出棧的,也意味着1是遍歷的起點。
咱們將它反向以後能夠獲得:
咱們再次從1出發能夠遍歷到2,3, 4,說明{1, 2, 3, 4}是一個強連通份量。
怎麼樣,整個過程是否是很是簡單?
咱們將這段邏輯用代碼實現,也並不會很複雜。
// Cpp // g 是原圖,g2 是反圖 void dfs1(int u) { vis[u] = true; for (int v : g[u]) if (!vis[v]) dfs1(v); s.push_back(u); } void dfs2(int u) { color[u] = sccCnt; for (int v : g2[u]) if (!color[v]) dfs2(v); } void kosaraju() { sccCnt = 0; for (int i = 1; i <= n; ++i) if (!vis[i]) dfs1(i); for (int i = n; i >= 1; --i) if (!color[s[i]]) { ++sccCnt; dfs2(s[i]); } }
# python N = 7 graph, rgraph = [[] for _ in range(N)], [[] for _ in range(N)] used = [False for _ in range(N)] popped = [] # 建圖 def add_edge(u, v): graph[u].append(v) rgraph[v].append(u) # 正向遍歷 def dfs(u): used[u] = True for v in graph[u]: if not used[v]: dfs(v) popped.append(u) # 反向遍歷 def rdfs(u, scc): used[u] = True scc.append(u) for v in rgraph[u]: if not used[v]: rdfs(v, scc) # 建圖,測試數據 def build_graph(): add_edge(1, 3) add_edge(1, 2) add_edge(2, 4) add_edge(3, 4) add_edge(3, 5) add_edge(4, 1) add_edge(4, 6) add_edge(5, 6) if __name__ == "__main__": build_graph() for i in range(1, N): if not used[i]: dfs(i) used = [False for _ in range(N)] # 將第一次dfs出棧順序反向 popped.reverse() for i in popped: if not used[i]: scc = [] rdfs(i, scc) print(scc)
算法講完,代碼也寫了,可是並無結束,仍然有一個很大的疑惑沒有解開。算法的原理很簡單,很容易學會,但問題是爲何這樣作就是正確的呢?這其中的原理是什麼呢?咱們彷佛仍然沒有弄得很是清楚。
這裏面的原理其實很簡單,咱們來思考一下,若是咱們在正向dfs的時候,u點出如今了v點的後面,也就是u點後於v點出棧。有兩種可能,一種多是u點能夠連通到v點,說明u是v的上游。還有一種多是u不能連通到v,說明圖被分割成了多個部分。對於第二種狀況咱們先不考慮,由於這時候u和v必定不在一個連通份量裏。對於第一種狀況,u是v的上游,說明u能夠連通到v。
這時候,咱們將圖反向,若是咱們從u還能夠訪問到v,那說明了什麼?很明顯,說明了在正向圖當中v也有一條路徑連向u,否則反向以後u怎麼連通到v呢?因此,u和v顯然是一個強連通份量當中的一個部分。咱們再把這個結論推廣,全部u能夠訪問到的,第一次遍歷時在它以前出棧的點,都在一個強連通份量當中。
若是你能理解了這一點,那麼整個算法對你來講也就豁然開朗了,相信剩下的細節也都不足爲慮了。
到這裏,整個算法流程的介紹就算是結束了,但願你們均可以enjoy今天的內容。