[C++圖論] 強連通

概念

在一個有向圖中,如果說有兩個點互相可達,那麼這兩個點就可說是 強連通。簡單來說,就是在這兩個點中有一個環,可以從其中一點任意抵達對面的另一點。當然,官方語言(度娘解釋)是這樣的:

強連通(Strongly Connected)是指一個有向圖(Directed Graph)中任意兩點v1、v2間存在v1到v2的路徑(path)及v2到v1的路徑。

那麼有了 強連通 這個概念,既然就會有 強連通圖 了。當然,這個其實也可以舉一反三:
就是在一個有向圖中,任意兩點都可以互相可達
接下來再貼度娘解釋:

強連通圖(Strongly Connected Graph)是指在有向圖G中,如果對於每一對vi、vj,vi≠vj,從vi到vj和從vj到vi都存在路徑,則稱G是強連通圖。一個有向圖是強連通的,當且僅當G中有一個迴路,它至少包含每個節點一次。

而強連通圖有兩個性質:

  1. 充分性:如果G中有一個迴路,它至少包含每個節點一次,則G中任兩個節點都是互相可達的,故G是強連通圖.
  2. 必要性:如果有向圖是強連通的,則任兩個節點都是相互可達。故必可做一回路經過圖中所有各點。若不然則必有一回路不包含某一結點v,並且v與迴路上的個節點就不是相互可達,與強連通條件矛盾.

當然,其實這個不懂也沒有關係,接着看下面的 強連通分量 吧。

有向圖的極大強連通子圖,稱爲強連通分量(strongly connected components)。

意思也就是說,在有向圖中的一些強連通子圖,需要注意的是: 一個點也算作一個強連通分量 哦。

只讀概念有些燒腦,讓我們一起來看個圖理解一哈:

在這裏插入圖片描述

在這個圖中,顯然1234(1 — 2 — 3 — 4)是最大的強連通分量,因爲從他們中的任意一點都可抵達其他的三個點 (大家可以試下哦)。然後 5566 分別是強連通分量。

實現

這裏會介紹兩種實現的方法,不過呢,會強烈安利第二種,而第一種則是介紹一下算法的流程。

Kosaraju

在這裏插入圖片描述
在這個圖中大家可以很顯然的看出有兩個強連通分量,分別是AA系列BB系列,可是如果說從A中的任意一點進入的話,就只需要一次就能夠將所有的點遍歷完,可是如果從B中的任意一點進入的話,則需要兩次DFS才能夠遍歷完所有的點,因此,
找到合適的起始點至關重要
所以,我們可以進行兩次DFS:

  1. 第一次選擇任一點進行DFS,然後按照遍歷順序將每個點依次存起來,按照深度越深的越後順序。
  2. 將圖G變成反圖G’。
  3. 按照遍歷順序從後往前遍歷G’,即從第一次DFS退出的那個點開始往回遍歷反圖。
  4. 在這次DFS中遍歷了幾次就說明有幾個強連通分量。

解釋一下,假設有兩個點a和b,如果說他們兩個互相強連通,那麼正圖從a能走到b的話,反圖也一定可以。
可是爲什麼算法一定要這樣呢?我找到一篇博客中認爲說的頗有道理,附下:

由圖(即爲上圖)可知:
不管從A開始DFS,還是從B開始DFS,因爲A到B有一條邊,所以 最後退出DFS的點一定在A上 ,若選最後退出DFS的點爲始點(位於A中)並對上圖的 反圖 進行一次DFS,則可以得到圖中的兩個強連通。

but,這種方法要用兩次DFS,數據一大則十分浪費時間,因此不太推薦。
而接下來的方法卻只需要一次DFS,相比起來會快上許多,而且也較好理解。

不過如果有人想學一下的話,推薦一篇博客,我認爲寫得也是比較好的哈。。

Tarjan

接下來就是重頭戲了!!!!
論tarjan這個人的話,那麼在後面好多地方都會有他 插上一腳 的算法,不可不謂是一個神人啊。
閒話少說,切入正題。

這裏有一張圖,顯然,這個有向圖的強連通分量應該是這幾個:
[外鏈圖片轉存失敗(img-hR4aEEH2-1563960651474)(https://i.loli.net/2019/06/05/5cf74fdfd0d1962775.png)]
因爲1-4中所有節點可以互相到達,而5只能到6,6卻不能到5,因此他們分別是不同的強連通分量。
可是,該如何實現這個呢?我們將定義兩個數組—— dfn[]dfn[ ]low[]low[ ]
dfn的意思是入棧的時間,也就是被訪問的時間(是第幾個被訪問的),大家也可以看做一個時間戳,而low的意思則是該節點或者是該節點的子節點所能到達的最小的時間標記,舉個例子:
low[u]low[u] 代表着以u爲根節點時,u或u的兒子在這棵樹中所能到達的最上面的地方(因爲時間標記是越到後面越大,因此取min的時候就是最先訪問到的節點)。

算法流程

  1. 首先,先將dfn[u]dfn[u]low[u]low[u]都打成新的時間戳。
  2. 枚舉u的兒子們v,如果說v沒有被訪問過,那麼就訪問他,並且修改low[u]low[u]變成v所能到達的最上面的點,即low[u]=min(low[u],low[v])low[u] = min (low[u], low[v])。否則,如果說v訪問過了,而且此時還在棧中沒有被彈出去(因爲如果說被彈出去了,就說明是其他的強連通分量,而不是這一個),那麼修改low[u]low[u]變成子節點的時間戳( 注意!!!不是子節點所能到達的最上面的點
  3. 遍歷結束後,如果說dfn[u]==low[u]dfn[u] == low[u],就說明不管怎麼遍歷,u和他的兒子最大都只能遍歷到他自己,也就代表着他是這個強連通分量的根,那麼此時就可以清棧了,棧頂一直到該節點都是這個強連通分量。

首先從1號節點遍歷,一直遍歷到6號,並且將它們分別壓入棧。一直到6號節點時,他已經沒有可遍歷的點了,且dfn=low=4dfn=low=4,那麼就把6退出,他是第一個強連通分量,回溯。
在這裏插入圖片描述
返回到5號節點,他也沒有節點可以訪問了,而且dfn=low=3dfn=low=3,5作爲第二個強連通分量,退出棧並且回溯。
在這裏插入圖片描述
返回到3號節點,還有4號節點可以訪問,那麼繼續訪問。訪問4號節點時,它還可以走到1號節點,那麼將4號節點的low改爲dfn[1]。沒有節點可以訪問,回溯。3號節點的low也修改爲1
在這裏插入圖片描述
返回到3號節點後,也沒有可以訪問得了,繼續回溯到1號節點,訪問2節點,2訪問到4時,4還在棧中,將low[2]改爲dfn[4]就是5。回溯
在這裏插入圖片描述
回溯到1後,low=dfn,說明這又是一個強連通分量,清棧。

參考代碼

void tarjan (int u){
    dfn[u] = low[u] = ++indx;
    S.push (u);
    instack[u] = 1;
    for (int i = 0; i < G[u].size (); i++){
        int v = G[u][i];
        if (!dfn[v]){
            tarjan (v);
            low[u] = min (low[u], low[v]);
        }
        else if (instack[v])
            low[u] = min (low[u], dfn[v]);
    }
    if (dfn[u] == low[u]){
        sum ++;
        int v;
        belong[u] = sum;
        do{
            v = S.top ();
            belong[v] = sum;
            instack[v] = 0;
            S.pop ();
        }while (u != v);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章