算法(第4版) Chapter 4.2 強聯通性 Tarjan算法補充

參考資料
http://blog.csdn.net/acmmmm/a...
https://www.byvoid.com/blog/s...
http://blog.csdn.net/nothi/ar...算法


在教材中有向圖的強連通只說起了一種,其實還有另外兩個經典的算法,所以作一個補充。學習

Tarjan算法

思路提點

  • tarjan的過程就是dfs過程測試

    • 對圖dfs一下,遍歷全部未遍歷過的點 ,會獲得一個有向樹,顯然有向樹是沒有環的。ui

    • (注意搜過的點不會再搜) 則能產生環的只有 指向已經遍歷過的點 的邊spa

20140417204036062

只有紅色與綠色邊有可能產生環。
對於深搜過程,咱們須要一個棧來保存當前所在路徑上的全部點(棧中全部點必定是有父子關係的)
再仔細觀察紅邊與綠邊,首先獲得結論:紅邊不產生環,綠邊產生環.net

  1. 對於紅邊,鏈接的兩個點三、7沒有父子關係,這種邊稱爲橫叉邊
    橫叉邊必定不產生環code

  2. 對於綠邊,鏈接的兩個點六、4是父子關係,這種邊稱爲後向邊
    環必定由後向邊產生blog

  3. 圖中除了黑色的樹枝邊,必定只有橫叉邊和後向邊(不存在其餘種類的邊)get


則如下考慮對於這兩種邊的處理和判斷:
20140417203338953it

Stack = {1,2,3}。3沒有多餘的其餘邊,所以3退棧,把3做爲一個強連通份量


再次深搜:
20140417203641265

此時棧 Stack = {1,2,7}
發現紅邊指向了已經遍歷過的點3 => 是上述的2種邊之一
而3不在棧中
=> 3點與7點無父子關係
=> 該邊爲橫叉邊
=> 採起無視法。

繼而7點退棧 產生連通份量{7}
繼而2點退棧 產生連通份量{2}


再次深搜:
20140417205508125

此時 Stack = {1,4,5,6}
發現綠邊指向了已經遍歷過的點4 => 是上述的2種邊之一
而4在棧中
=> 4點與6點是父子關係
=> 該邊爲後向邊
=> 4->6的路徑上的點都是環。


實際狀況可能更復雜:
20140417210343578

出現了大環套小環的狀況,顯然咱們認爲最大環是一個強連通份量(即:{4,5,6,8} )

於是咱們須要強化一下dfs過程,增添幾個變量來記錄父節點和後向邊的狀況

定義:

int dfn[N], low[N];

  • dfn[i] 表示 遍歷到 i 點時是第幾回dfs (有時也叫時間戳)

  • low[u] 表示 u 的子樹 能鏈接到 [棧中] 最上端的點 的dfn值(換句話說,也就是最小的dfn)

Stack stack 上述的棧
int BelongTo[N] 強連通份量的ID

通俗語言解讀:

  • dfn[i] 即我就是我,是數字不同的煙火。每一個點的ID(不是強連通份量的ID,而是每一個點本身的身份標識ID),按時間順序賦值。好比第一個尋訪的dfn的值爲 1,第二個尋訪的DFN的值爲 2,以此類推。能夠經過比較大小來判斷是爸爸仍是兒子。(是後向邊仍是橫插邊?)

  • low[u] 若是是後向邊的話連到哪一個爸爸?記錄下來爸爸的ID。

  • Stack 怎麼判斷是否是後向邊呢?—>看在不在棧內。

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(v)而不是low(v)?????????
}

當dfn(u)==low(u)時,以u爲根的搜索子樹上全部節點是一個強連通份量。

  • 緣由

    • 在算法開始的時候,咱們把 i 圧入棧中。根據 low[i] 和 dfn[i] 的定義咱們知道,

    • 若是 low[i] < dfn[i],則以 i 爲頂點的子樹中,有指向祖先的後向邊,則說明 i 和 i 的父親爲在同一連通分支,也就是說留在棧中的元素都是和父結點在同一連通分支的。

    • 若是 low[i] == dfn[i],則 i 爲頂點的子樹中沒有後向邊,那麼因爲 留在棧中的元素都是和父結點在同一連通分支的,咱們能夠知道,從棧頂到元素 i 構成了一個連通分支。顯然,low[i]不可能小於dfn[i]


算法僞代碼以下

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 stack)                   // 若是節點v還在棧內
            Low[u] = min(low[u], dfn[v])
    if (dfn[u] == low[u])                      // 若是節點u是強連通份量的根
        repeat
            v = stack.pop                  // 將v退棧,爲該強連通份量中一個頂點
            print v
        until (u== v)
}

Tarjan JAVA代碼

  • 複雜度

    • 時間:O(N+M)

  • 與Trajan算法相比,Kosaraju算法可能會稍微更直觀一些。可是Tarjan只用對原圖進行一次DFS,不用創建逆圖,更簡潔。在實際的測試中,Tarjan算法的運行效率也比Kosaraju算法高30%左右。此外,該Tarjan算法與求無向圖的雙連通份量(割點、橋)的Tarjan算法也有着很深的聯繫。學習該Tarjan算法,也有助於深刻理解求雙連通份量的Tarjan算法,二者能夠類比、組合理解。

public class Tarjan {
    private int[] id; // 強連通ID
    private int index_id = 0; // 強連通ID計數器
    private int[] dfn, low; //時間戳計數器
    private int index_dfn = 1; //時間戳初始化時默認爲0。所以從1開始賦值,以區別是否訪問過。
    private Stack<Integer> stack= new Stack<Integer>();
    private boolean[] onStack;
    
    public Tarjan(Digraph G) {
        onStack = new boolean[G.V()];
        id = new int[G.V()];
        dfn = new int[G.V()];
        low = new int[G.V()];

        for (int s = 0; s < G.V(); s++)
            if (dfn[s]==0) {
                tarjan(G, s);
                index_id++;
            }
    }

    private void tarjan(Digraph G, int u) {
        stack.push(u); //壓入棧
        onStack[u] = true; //方便以後判斷
        dfn[u] = low[u] = ++index_dfn; //時間戳賦值,並表示訪問過了
        for (int w : G.adj(u)) {
            if (dfn[w] == 0) { //若w未被訪問過
                tarjan(G, w); //繼續深搜
                low[u] = Math.min(low[w], low[u]); //回溯,當前點u 選取包括本身的子樹中最小的low值
            } else if (onStack[w] && dfn[w]<low[u]) { //若是w是後向邊
                low[u] = dfn[w] // 更新low值
            }
        }

        //退棧並記錄強連通ID
        if (low[u] == dfn[u]) { 
            while (!stack.isEmpty()) {
                int top = stack.pop();
                id[top] = index_id;
                onStack[top] = false;
            }
        }
    }

    public boolean stronglyConnected(int v, int w) {
        return id[v] == id[w];
    }

    public int id(int v) {
        return id[v];
    }

    public int count() {//參照書上的,感受返回的不是強連通的個數N,由於從零開始計數,因此返回的是N-1
        return index_id ;
    }


}

簡單證實

首先,這邊再重複一下什麼是後向邊:就是在深度優先搜索中,子孫指向祖先的邊。
在一棵深度優先搜索樹中,對於結點v, 和其父親結點u而言,u,v 屬於同一個強連通分支的充分必要條件
以v爲根的子樹中,有一條後向邊指向u或者u的祖先

一、必要性

若是 u, v 屬於同一個強連通分支則一定存在一條 u -> v 的路徑和一條 v -> u的路徑。
合併兩條則有 u->v->v1->v2->..vn->u, 若頂點v1到vn都是v 的子孫,則有 vn->u這樣一條後向邊。
若是v1到vn 不全是vn的子孫,則一定有一個是u的祖先,咱們不妨設vi爲u的祖先,則有一條後向邊 V[i-1] ->v[i]。

二、充分性

咱們設 u1->u2->u3..->un->u->v->v1->v2..->vn。咱們假設後向邊vn指向ui則有這樣一個環:u[i]->u[i+1]...->u->v->v1->v2..->v[n-1]->v[n]->u[i]。易知,有一條u->v的路徑,同時有v->u的路徑。固u,v屬於同一連通分支。

相關文章
相關標籤/搜索