kosaraju 算法

前言

以前學習了算法,但是因爲沒有記錄下來,最近又要重新開始學習了,這次就將我的學習經歷彙總成文章,記錄下來。

科薩拉朱算法(英語:Kosaraju's algorithm),也被稱爲科薩拉朱—夏爾算法,是一個在線性時間內尋找一個有向圖中的強連通分量的算法。

首先我們需要知道幾個概念

有向圖

邊爲有方向的圖稱作有向圖(英語:directed graphdigraph)。

有向圖的一種比較嚴格的定義是這樣的:一個二元組 G=(V,E) ,其中

  • V 是節點的集合;
  • (也稱爲有向邊,英語:directed edgedirected link;或,英語:arcs)的集合,其中的元素是節點的有序對。

下圖是一個簡單的有向圖:

image

強連通分量

有向圖中,儘可能多的若干頂點組成的子圖中,這些頂點都是相互可到達的,則這些頂點成爲一個強連通分量。

image

其實求解強連通分量的算法並不止一種,除了Kosaraju之外還有大名鼎鼎的Tarjan算法可以用來求解。但相比Tarjan算法,Kosaraju算法更加直觀,更加容易理解

DFS 生成樹

先來了解 DFS 生成樹,我們以下面的有向圖爲例:

image

有向圖的 DFS 生成樹主要有 4 種邊(不一定全部出現):

  1. 樹邊(tree edge):示意圖中以黑色邊表示,每次搜索找到一個還沒有訪問過的結點的時候就形成了一條樹邊。
  2. 反祖邊(back edge):示意圖中以紅色邊表示(即7 - 1),也被叫做回邊,即指向祖先結點的邊。
  3. 橫叉邊(cross edge):示意圖中以藍色邊表示(即 9-7),它主要是在搜索的時候遇到了一個已經訪問過的結點,但是這個結點 並不是 當前結點的祖先。
  4. 前向邊(forward edge):示意圖中以綠色邊表示(即 3-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)

動畫演示

image

動畫演示和標準的 Kosaraju 算法有點不一樣:它是先 DFS 遍歷頂點得到逆後序排序,然後再將有向圖置爲反向圖,按照逆後序排序取出頂點,深度優先搜索反向圖。結果和 Kosaraju 算法一致。

引用、推薦

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章