1. 程式人生 > 其它 >Kosaraju演算法、Tarjan演算法分析及證明--強連通分量的線性演算法

Kosaraju演算法、Tarjan演算法分析及證明--強連通分量的線性演算法

一、背景介紹

強連通分量是有向圖中的一個子圖,在該子圖中,所有的節點都可以沿著某條路徑訪問其他節點。強連通性是一種非常重要的等價抽象,因為它滿足

  • 自反性:頂點V和它本身是強連通的
  • 對稱性:如果頂點V和頂點W是強連通的,那麼頂點W和頂點V也是強連通的
  • 傳遞性:如果V和W是強連通的,W和X是強連通的,那麼V和X也是強連通的

強連通性可以用來描述一系列屬性,如自然界中物種之間的捕食關係,互相捕食的物種可以看作等價的,在自然界能量傳遞中處於同一位置。

下圖中,子圖{1,2,3,4}為一個強連通分量,因為頂點1,2,3,4兩兩可達。{5},{6}也分別是兩個強連通分量。

二、Kosaraju演算法描述

Kosaraju演算法通過以下步驟獲得一個有向圖的強連通分量。

  • 在圖G中,計算圖G的反向圖G'的深度優先搜尋的逆後序排列。反向圖是比如原圖中有V到W的連結,那麼反向圖中就有W到V的連結。逆後序排列是指,在對一個圖進行深度優先遍歷時,先進行子節點的遞迴,再將本節點加入到一個棧中,最後依次出棧以獲得的序列。逆後序排列有一個重要特性,就是如果有W到V的有向連結,那麼實際出棧時,W仍然排在V的前面
  • 在G中進行普通的深度優先搜尋,但是搜尋順序不是按照正統的( i = 0, i < N )去依次搜尋,而是以剛剛獲得的逆後序排列的順序進行搜尋。
  • 每個以這個逆後序排列中的元素開始的DFS搜尋,找到的所有元素,都是同一個強聯通分量的元素。

為什麼這個演算法可以獲得強連通分量呢?網上的證明很少,所以下面給出我的邏輯證明。

三、Kosaraju演算法證明

我們按照演算法描述的步驟往下走:

  1. 按照演算法的結論,假設我們已經獲得了一個逆後序排列,我們從中找兩個元素,分別是V,W,W先出棧並且通過DFS找到了V。那麼,V和W就是同一個聯通分量的元素。到底是不是呢?
  2. 不管是不是,我們至少可以確定對於該有向圖G,W有一條連結通往V,我們記作W->V。那麼,對於該有向圖的反向圖G',確定有連結V->W
  3. 我們開始思考,在什麼條件下,我們能夠在反向圖 G' 中獲得V...W(即W先出棧)這樣一個排列呢?要知道,我們剛剛確定了有連結V->W,所以逆後序排列中,應該是V排在W的前面,W...V這樣啊?
  4. 所以在G'中,要麼是我們之前提到的,在V->W的同時有W->V的連結;要麼就是W和V之間沒有任何聯絡,這樣V的DFS結束之後,包含W的聯通分量的DFS才開始遍歷,才能造成W比V先出棧
  5. 但是我們已經知道,V和W不是毫無關係的,確定有連結V->W,所以第二個可能不成立,所以必然存在一個W->V的連結,也就是W和V是互相聯通的!
  6. 證明完畢。

四、演算法原始碼

 因為程式碼很長,放在github上了。程式碼是在Idea中編譯執行通過的,實現了一個基本的Graph資料結構,在此基礎上實現了Kosaraju類,供參考。

原始檔


五、更加迅速的tarjan演算法

部分內容轉自https://www.byvoid.com/blog/scc-tarjan

1.Tarjan演算法

Tarjan演算法是基於對圖深度優先搜尋的演算法,每個強連通分量為搜尋樹中的一棵子樹。搜尋時,把當前搜尋樹中未處理的節點加入一個堆疊,回溯時可以判斷棧頂到棧中的節點是否為一個強連通分量。

定義DFN(u)為節點u搜尋的次序編號(時間戳),Low(u)為u或u的子樹能夠追溯到的最早的棧中節點的次序號。由定義可以得出,

Low(u)=Min
{
    DFN(u),
    Low(v),(u,v)為樹枝邊,u為v的父節點
    DFN(v),(u,v)為指向棧中節點的後向邊(非橫叉邊)
}

當DFN(u)=Low(u)時,以u為根的搜尋子樹上所有節點是一個強連通分量。

演算法虛擬碼如下

tarjan(u)
{
    DFN[u]=Low[u]=++Index                      // 為節點u設定次序編號和Low初值
    Stack.push(u)                              // 將節點u壓入棧中
    for each (u, v) in E                       // 列舉每一條邊
        if (v is not visted)               // 如果節點v未被訪問過
            tarjan(v)                  // 繼續向下找
            Low[u] = min(Low[u], Low[v])
        else if (v in S)                   // 如果節點v還在棧內
            Low[u] = min(Low[u], DFN[v])
    if (DFN[u] == Low[u])                      // 如果節點u是強連通分量的根
        repeat
            v = S.pop                  // 將v退棧,為該強連通分量中一個頂點
            print v
        until (u== v)
}

2.演算法的精要之處如下:

  • 為每個加入的節點設定序號,使得後搜尋到的節點的序號一定高於前面的節點
  • 那麼,如果後搜尋到的節點的子節點裡居然有比它本身還要小的節點,則一定出現了環。有環則必定強連通
  • 那麼,把該節點的標識節點Low(u)設為發現的後向節點的值DFN(V)
  • 然後遞迴程式返回該節點的上級節點u-1,上級節點判斷Low(u-1)的值,也把它指向了剛剛找到的後向節點
  • 最後,除了後向節點本身,所有環中的節點都指向該後向節點,那麼,我們找到了一個強連通分量。

3.演算法流程的演示

從節點1開始DFS,把遍歷到的節點加入棧中。搜尋到節點u=6時,DFN[6]=LOW[6],找到了一個強連通分量。退棧到u=v為止,{6}為一個強連通分量。

返回節點5,發現DFN[5]=LOW[5],退棧後{5}為一個強連通分量。

返回節點3,繼續搜尋到節點4,把4加入堆疊。發現節點4向節點1有後向邊,節點1還在棧中,所以LOW[4]=1。節點6已經出棧,(4,6)是橫叉邊,返回3,(3,4)為樹枝邊,所以LOW[3]=LOW[4]=1。

繼續回到節點1,最後訪問節點2。訪問邊(2,4),4還在棧中,所以LOW[2]=DFN[4]=5。返回1後,發現DFN[1]=LOW[1],把棧中節點全部取出,組成一個連通分量{1,3,4,2}。

至此,演算法結束。經過該演算法,求出了圖中全部的三個強連通分量{1,3,4,2},{5},{6}。

可以發現,執行Tarjan演算法的過程中,每個頂點都被訪問了一次,且只進出了一次堆疊,每條邊也只被訪問了一次,所以該演算法的時間複雜度為O(N+M)。

求有向圖的強連通分量還有一個強有力的演算法,為Kosaraju演算法。Kosaraju是基於對有向圖及其逆圖兩次DFS的方法,其時間複雜度也是O(N+M)。與Trajan演算法相比,Kosaraju演算法可能會稍微更直觀一些。但是Tarjan只用對原圖進行一次DFS,不用建立逆圖,更簡潔。在實際的測試中,Tarjan演算法的執行效率也比Kosaraju演算法高30%左右。此外,該Tarjan演算法與求無向圖的雙連通分量(割點、橋)的Tarjan演算法也有著很深的聯絡。學習該Tarjan演算法,也有助於深入理解求雙連通分量的Tarjan演算法,兩者可以類比、組合理解。