以前學習了演算法,但是因為沒有記錄下來,最近又要重新開始學習了,這次就將我的學習經歷彙總成文章,記錄下來。
科薩拉朱演算法(英語:Kosaraju's algorithm),也被稱為科薩拉朱—夏爾演算法,是一個在線性時間內尋找一個有向圖 "圖 (數學)")中的強連通分量的演算法。
首先我們需要知道幾個概念
有向圖
邊為有方向的圖稱作有向圖(英語:directed graph或digraph)。
有向圖的一種比較嚴格的定義是這樣的:一個二元組\( G=(V,E) \),其中
- \( V \)是節點的集合;
- \( {\displaystyle E\subseteq {(x,y)\mid (x,y)\in V^{2}\wedge x\neq y}} \)是邊(也稱為有向邊,英語:directed edge或directed link;或弧,英語:arcs)的集合,其中的元素是節點的有序對。
下圖是一個簡單的有向圖:
強連通分量
有向圖中,儘可能多的若干頂點組成的子圖中,這些頂點都是相互可到達的,則這些頂點成為一個強連通分量。
其實求解強連通分量的演算法並不止一種,除了Kosaraju之外還有大名鼎鼎的Tarjan演算法可以用來求解。但相比Tarjan演算法,Kosaraju演算法更加==直觀==,更加==容易理解==。
DFS 生成樹
先來了解 DFS 生成樹,我們以下面的有向圖為例:
有向圖的 DFS 生成樹主要有 4 種邊(不一定全部出現):
- 樹邊(tree edge):示意圖中以黑色邊表示,每次搜尋找到一個還沒有訪問過的結點的時候就形成了一條樹邊。
- 反祖邊(back edge):示意圖中以紅色邊表示(即 \( 7 \rightarrow 1 \)),也被叫做回邊,即指向祖先結點的邊。
- 橫叉邊(cross edge):示意圖中以藍色邊表示(即 \( 9 \rightarrow 7 \)),它主要是在搜尋的時候遇到了一個已經訪問過的結點,但是這個結點 並不是 當前結點的祖先。
- 前向邊(forward edge):示意圖中以綠色邊表示(即 \( 3 \rightarrow 6 \)),它是在搜尋的時候遇到子樹中的結點的時候形成的。
這是使用 js 實現的一個簡單的 DFS:
const depth1 = (dom, nodeList) => {
dom.children.forEach((element) => {
depth1(element, nodeList)
})
nodeList.push(dom.name)
}
我們考慮 DFS 生成樹與強連通分量之間的關係。
如果結點 \( u \) 是某個強連通分量在搜尋樹中遇到的第一個結點,那麼這個強連通分量的其餘結點肯定是在搜尋樹中以 \( u \) 為根的子樹中。結點 \( u \) 被稱為這個強連通分量的根。
反證法:假設有個結點 \( v \) 在該強連通分量中但是不在以 $u$ 為根的子樹中,那麼 \( u \) 到 \( v \) 的路徑中肯定有一條離開子樹的邊。但是這樣的邊只可能是橫叉邊或者反祖邊,然而這兩條邊都要求指向的結點已經被訪問過了,這就和 \( u \) 是第一個訪問的結點矛盾了。得證。
Kosaraju 演算法
該演算法依靠兩次簡單的 DFS 實現:
第一次 DFS,選取任意頂點作為起點,遍歷所有未訪問過的頂點,並在回溯之前給頂點編號,也就是後序遍歷。
第二次 DFS,對於反向後的圖,以標號最大的頂點作為起點開始 DFS。這樣遍歷到的頂點集合就是一個強連通分量。對於所有未訪問過的結點,選取標號最大的,重複上述過程。
兩次 DFS 結束後,強連通分量就找出來了,Kosaraju 演算法的時間複雜度為 \( O(n+m) \) 。
這裡利用下網上的演算法,簡單表示一下:
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)
動畫演示
動畫演示和標準的 Kosaraju
演算法有點不一樣:它是先 DFS
遍歷頂點得到逆後序排序,然後再將有向圖置為反向圖,按照逆後序排序取出頂點,深度優先搜尋反向圖。結果和 Kosaraju
演算法一致。
引用、推薦
- https://xie.infoq.cn/article/02144dc8c84e4b85cc9b27779
- https://zh.wikipedia.org/wiki/%E5%9B%BE_(%E6%95%B0%E5%AD%A6)#%E6%9C%89%E5%90%91%E5%9B%BE
- https://oi-wiki.org/graph/scc/#kosaraju-%E7%AE%97%E6%B3%95
- https://www.cnblogs.com/nullzx/p/6437926.html
- https://www.cnblogs.com/RioTian/p/14026585.html
- https://www.youtube.com/watch?v=TyWtx7q2D7Y
- https://www.youtube.com/watch?v=R6uoSjZ2imo
- https://redspider110.github.io/2018/08/22/0093-algorithms-scc...