連通圖演算法詳解之① :Tarjan 和 Kosaraju 演算法

RioTian發表於2020-08-06

相關閱讀: 雙連通分量割點和橋

簡介

在閱讀下列內容之前,請務必瞭解 圖論相關概念 中的基礎部分。

強連通的定義是:有向圖 G 強連通是指,G 中任意兩個結點連通。

強連通分量(Strongly Connected Components,SCC)的定義是:極大的強連通子圖。

這裡想要介紹的是如何來求強連通分量。

Tarjan 演算法發明人

Robert E. Tarjan (1948~) 美國人。

你是不是感覺Robert E. Tarjan 這個名字很熟悉?

沒錯,Robert E. Tarjan和John E. Hopcroft就是發明了深度優先搜尋的兩個人——1986年的圖靈獎得主。

除此之外 Tarjan 發明了很多演算法結構。光 Tarjan 演算法就有很多,比如求各種連通分量的 Tarjan 演算法,求 LCA(Lowest Common Ancestor,最近公共祖先)的 Tarjan 演算法。並查集、Splay、Toptree 也是 Tarjan 發明的。

你看牛人們從來都不閒著的。他們到處交流,尋找合作伙伴,一起改變世界。

我們這裡要介紹的是他的在有向圖中求強連通分量的 Tarjan 演算法。

另外,Tarjan 的名字 j 不發音,中文譯為塔揚。

DFS 生成樹

在介紹該演算法之前,先來了解 DFS 生成樹 ,我們以下面的有向圖為例:

scc1.png

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

  1. 樹邊(tree edge):綠色邊,每次搜尋找到一個還沒有訪問過的結點的時候就形成了一條樹邊。
  2. 反祖邊(back edge):黃色邊,也被叫做回邊,即指向祖先結點的邊。
  3. 橫叉邊(cross edge):紅色邊,它主要是在搜尋的時候遇到了一個已經訪問過的結點,但是這個結點 並不是 當前結點的祖先時形成的。
  4. 前向邊(forward edge):藍色邊,它是在搜尋的時候遇到子樹中的結點的時候形成的。

我們考慮 DFS 生成樹與強連通分量之間的關係。

如果結點 \(u\) 是某個強連通分量在搜尋樹中遇到的第一個結點,那麼這個強連通分量的其餘結點肯定是在搜尋樹中以 \(u\) 為根的子樹中。 \(u\) 被稱為這個強連通分量的根。

反證法:假設有個結點 \(v\) 在該強連通分量中但是不在以 \(u\) 為根的子樹中,那麼 \(u\)\(v\) 的路徑中肯定有一條離開子樹的邊。但是這樣的邊只可能是橫叉邊或者反祖邊,然而這兩條邊都要求指向的結點已經被訪問過了,這就和 \(u\) 是第一個訪問的結點矛盾了。得證。

Tarjan 演算法求強連通分量

Tarjan 演算法中為每個結點 \(u\) 維護了以下幾個變數:

  1. \(dfn[u]\) :深度優先搜尋遍歷時結點 \(u\) 被搜尋的次序。

  2. \(low[u]\) :設以 \(u\) 為根的子樹為 \(Subtree(u)\)\(low[u]\) 定義為以下結點的 \(dfn\) 的最小值: \(Subtree(u)\) 中的結點;從 \(Subtree(u)\) 通過一條不在搜尋樹上的邊能到達的結點。

    ps:每次找到一個新點,這個點\(low\ []=dfn\ []\)

一個結點的子樹內結點的 dfn 都大於該結點的 dfn。

從根開始的一條路徑上的 dfn 嚴格遞增,low 嚴格非降。

按照深度優先搜尋演算法搜尋的次序對圖中所有的結點進行搜尋。在搜尋過程中,對於結點 \(u\) 和與其相鄰的結點 \(v\) (v 不是 u 的父節點)考慮 3 種情況:

  1. \(v\) 未被訪問:繼續對 \(v\) 進行深度搜尋。在回溯過程中,用 \(low[v]\) 更新 \(low[u]\) 。因為存在從 \(u\)\(v\) 的直接路徑,所以 \(v\) 能夠回溯到的已經在棧中的結點, \(u\) 也一定能夠回溯到。
  2. \(v\) 被訪問過,已經在棧中:即已經被訪問過,根據 \(low\) 值的定義(能夠回溯到的最早的已經在棧中的結點),則用 \(dfn[v]\) 更新 \(low[u]\)
  3. \(v\) 被訪問過,已不在在棧中:說明 \(v\) 已搜尋完畢,其所在連通分量已被處理,所以不用對其做操作。

將上述演算法寫成虛擬碼:

TARJAN_SEARCH(int u)
    vis[u]=true
    low[u]=dfn[u]=++dfncnt 		// 為節點u設定次序編號和Low初值
    push u to the stack 		// 將節點u壓入棧中
    for each (u,v) then do 		// 列舉每一條邊
        if v hasn't been search then  // 如果節點v未被訪問過
            TARJAN_SEARCH(v) 	      // 繼續向下搜尋
            low[u]=min(low[u],low[v]) // 回溯
        else if v has been in the stack then // 如果節點u還在棧內
            low[u]=min(low[u],dfn[v])
    if (DFN[u] == Low[u]) // 如果節點u是強連通分量的根
   repeat v = S.pop  	// 將v退棧,為該強連通分量中一個頂點
   print v
   until (u== v)

對於一個連通分量圖,我們很容易想到,在該連通圖中有且僅有一個 \(dfn[u]=low[u]\) 。該結點一定是在深度遍歷的過程中,該連通分量中第一個被訪問過的結點,因為它的 DFN 值和 LOW 值最小,不會被該連通分量中的其他結點所影響。

因此,在回溯的過程中,判定 \(dfn[u]=low[u]\) 的條件是否成立,如果成立,則棧中從 \(u\) 後面的結點構成一個 SCC (強連通分量)。

實現

int dfn[N], low[N], dfncnt, s[N], in_stack[N], tp;
int scc[N], sc;  // 結點 i 所在 scc 的編號
int sz[N];       // 強連通 i 的大小
void tarjan(int u) {
  low[u] = dfn[u] = ++dfncnt, s[++tp] = u, in_stack[u] = 1;
  for (int i = h[u]; i; i = e[i].nex) {
    const int &v = e[i].t;
    if (!dfn[v]) {
      tarjan(v);
      low[u] = min(low[u], low[v]);
    } else if (in_stack[v]) {
      low[u] = min(low[u], dfn[v]);
    }
  }
  if (dfn[u] == low[u]) {
    ++sc;
    while (s[tp] != u) {
      scc[s[tp]] = sc;
      sz[sc]++;
      in_stack[s[tp]] = 0;
      --tp;
    }
    scc[s[tp]] = sc;
    sz[sc]++;
    in_stack[s[tp]] = 0;
    --tp;
  }
}

時間複雜度 \(O(n + m)\)

Kosaraju 演算法

Kosaraju 演算法依靠兩次簡單的 DFS 實現。

第一次 DFS,選取任意頂點作為起點,遍歷所有未訪問過的頂點,並在回溯之前給頂點編號,也就是後序遍歷。

第二次 DFS,對於反向後的圖,以標號最大的頂點作為起點開始 DFS。這樣遍歷到的頂點集合就是一個強連通分量。對於所有未訪問過的結點,選取標號最大的,重複上述過程。

兩次 DFS 結束後,強連通分量就找出來了,Kosaraju 演算法的時間複雜度為 \(O(n+m)\)

實現

// 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]);
    }
}

Garbow 演算法

\(Tarjan\) 演算法和 \(Garbow\) 演算法是同一個思想的不同實現,但是 \(Garbow\) 演算法更加精妙,時間更少,不用頻繁更新 $ low $。

應用

我們可以將一張圖的每個強連通分量都縮成一個點。

然後這張圖會變成一個 DAG(為什麼?)。

DAG 好啊,能拓撲排序了就能做很多事情了。

舉個簡單的例子,求一條路徑,可以經過重複結點,要求經過的不同結點數量最多。

推薦題目

USACO Fall/HAOI 2006 受歡迎的牛

POJ1236 Network of Schools

相關文章推薦

清晰的圖示:https://www.byvoid.com/zhs/blog/scc-tarjan,

視覺化過程(英文講解):https://www.youtube.com/watch?v=TyWtx7q2D7Y

Garbow演算法:https://blog.csdn.net/zhouzi2018/article/details/81623747

其它

文章開源在 Github - blog-articles,點選 Watch 即可訂閱本部落格。 若文章有錯誤,請在 Issues 中提出,我會及時回覆,謝謝。

如果您覺得文章不錯,或者在生活和工作中幫助到了您,不妨給個 Star,謝謝。

(文章完)

相關文章