1. 程式人生 > >[雙連通分量] tarjan演算法

[雙連通分量] tarjan演算法

在前面說的話

其實這個借鑑了網上的一些教程/總結,但是主要還是看lrj的藍書並有部分引用以及自己的一些理解而成的,僅僅是為了給自己或他人總結用的,而不希望用於任何其他的用途亦或是被說抄襲。

引入

首先先來看幾個概念
割點(割頂、關節點),在一個無向圖中,如果刪除了某一個點,能使連通分量的個數增加, 那麼稱這個點為割點
特殊的情況:對於一個連通圖,割點就是在刪除以後能讓原圖不再連通的點
橋,在一個無向圖中,刪除了一條邊,能使連通分量個數增加,則這條邊為橋
那麼首先先來看看如何在一個無向圖中求出割點
暴力
暴力顯然是首先想到的,在一個無向圖中,嘗試刪除每一個點(邊),然後做一次dfs檢查連通分量的個數是否增加。
但很顯然這種辦法很慢,預計的時間複雜度應為O

(n(n+m))
lrj藍書上的基於DFS的做法
其實這種做法並不一定是lrj首創,但在這裡也沒有去找原作者,而lrj也沒有提,或許是人人皆知的做法吧(其實也可能是tarjan演算法的基礎)
首先這個演算法需要時間戳,於是這裡用pre記錄第一次訪問的時間戳
那麼DFS會產生一個DFS樹,在這棵樹中,又有幾個概念要認識
首先是樹邊,就是DFS樹上的邊,但是除了樹邊還會有反向邊,即由後代連回祖先的邊。
那麼考慮出現什麼情況才能證明是割點呢。先考慮一棵樹的樹根,顯然,當樹根下有多個子樹時,它會是割點,但當只有一棵子樹時,它不是割點。
然後考慮下面的點,這裡有定理
非根節點u是圖G的割點時,當且僅當u存在一個子節點及該子節點的後代不存在連向u
的祖先的反向邊。證明
很顯然,如果u的任何一個子節點都能夠連線到u的祖先,那麼砍掉u,它依然連通,如果有一個或是更多子節點不能連回u的祖先而只是連回u,那麼這一棵子樹在u被去掉後會成為一個單獨的連通塊,因此u是一個割點。
幹說不好懂,那麼上圖。
這裡寫圖片描述
那麼顯然地,如果在v以及其子樹中,最多連回u,那麼在u被砍掉之後,這塊就獨立了2333
試想,如果連回了u的祖先,那麼這棵dfs樹,可以在u被刪除後,沿著這條反向邊將這一塊變成另外一棵子樹,所以原圖並沒有增加連通分量,即u不是割點。
那麼如果用low(u)來表示從u及其後代中能夠通過反向邊連回的最前的祖先,也就是pre值最小的那個
那麼這個定理可以簡寫為當u
存在一個子節點v,使得low(v)pre(u)時,u為割點
那麼在這裡其實還有一種特殊情況,當low(v)>pre(u), 也就是連u都沒連回,可以腦補一下上面的圖,把那條反向邊連到v上,我們可以發現這是uv之間的邊其實就是原圖的一個橋
程式碼還是比較容易寫的,而且在後面求雙連通分量的tarjan演算法的思路基本是一致的。

雙連通分量

那麼依舊,先來看看定義
什麼是雙連通分量呢?
就是說,在一個連通圖中,任意兩個點都能通過至少兩條不經過同一個點的路徑而互相到達,就說明它是點雙連通的。
同樣舉幾個栗子例子
這裡寫圖片描述
我們暫且稱這樣的圖叫鐵三角好了23333
在這個圖中1和2可以通過直接到達和走經過3的彎路而到達,1和3,2和3同理,所以這是一個點雙連通的圖,點雙連通的判定條件比較複雜,這就涉及到我們的tarjan演算法了,但是我們先放一放,看看邊雙連通。
如果是邊雙連通呢,那要求就更加簡單,只要兩條路徑之間沒有公共的邊即可,就算經過同一個點也沒關係。因此我們發現,只要一個圖是點雙連通的,就一定是邊雙連通的,那麼判定就更加簡單,只要是沒有橋的一個圖就好了。
那麼雙連通分量就是一個無向圖中的雙連通的一個極大子圖,只要多畫了幾個圖就會發現,割點總是多個點雙連通分量的公共點,而邊雙連通分量則不會有公共點。
所以tarjan演算法的基本思路就是,用找割點的辦法, 凡是找到了一個割點,並且在這一棵子樹會在這個點被去掉之後單獨獨立出來的,就是一個點雙連通分量。因此這裡用一個棧來儲存訪問過的邊,直到發現了就不停地出棧,直到訪問回這條邊的時候就停止出棧,因為前面的邊就是和祖先是一個雙連通分量或是獨立的了。那麼程式碼和前面求割點的基本一致,只是在dfs處稍微多加了幾句,還是比較好寫的。

參考程式碼

#include <stack>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>

using namespace std;

const int MAXN = 1010,
          MAXM = 100010;

struct Edge {
    int u, v;
    int ne;
} e[MAXM * 2];

int pre[MAXN], bcc_cnt, dfs_clock, bccno[MAXN];
// 用bcc_cnt來記錄有多少個雙連通分量 bccno則是每一個點處於哪一個雙連通分量中,對於割點bccno無意義,儲存的是最後一個雙連通分量的編號
bool iscut[MAXN];

int head[MAXN];

vector  bcc[MAXN];
stack  S;

int dfs(int u, int fa) {
    int lowu = pre[u] = ++dfs_clock;
    int child = 0;

    for(int i = head[u]; ~i; i = e[i].ne) {
        int v = e[i].v;
        if(!pre[v]) { // 未訪問 
            S.push(e[i]);
            child++;
            int lowv = dfs(v, u);
            lowu = min(lowu, lowv);
            if(lowv >= pre[u]) { // 如果找到了雙連通分量 
                iscut[u] = 1;
                ++bcc_cnt;
                bcc[bcc_cnt].clear();
                for(;;) { // 出棧
                    Edge x = S.top();
                    S.pop();
                    if(bccno[x.u] != bcc_cnt) {
                        bcc[bcc_cnt].push_back(x.u);
                        bccno[x.u] = bcc_cnt;
                    }
                    if(bccno[x.v] != bcc_cnt) {
                        bcc[bcc_cnt].push_back(x.v);
                        bccno[x.v] = bcc_cnt;
                    }
                    if(x.u == u && x.v == v) {
                        break;
                    }
                }
            }
        } else {
            if(pre[v] < pre[u] && v != fa) {
                S.push(e[i]);
                lowu = min(lowu, pre[v]); // 用反向邊更新自己
            }
        }
    }

    if(fa < 0 && child == 1) {
        iscut[u] = 0;
    }
    return lowu;
}

void find_bcc(int n) {
    memset(pre, 0, sizeof pre);
    memset(iscut, 0, sizeof iscut);
    memset(bccno, 0, sizeof bccno);
    dfs_clock = bcc_cnt = 0;
    for(int i = 0; i < n; ++i) {
        if(!pre[i]) {
            dfs(i, -1);
        }
    }
}

int main(void) {
    int n, m;
    int cnt = 0;
    memset(head, -1, sizeof head);
    scanf("%d%d", &n, &m);
    for(int i = 0; i < m; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        e[cnt].u = u;
        e[cnt].v = v;
        e[cnt].ne = head[u];
        head[u] = cnt++;
        e[cnt].u = v;
        e[cnt].v = u;
        e[cnt].ne = head[v];
        head[v] = cnt++;
    }
    find_bcc(n);
    printf("%d\n", bcc_cnt);
    for(int i = 1; i <= bcc_cnt; ++i) {
        printf("%d : ", i);
        for(int j = 0; j < bcc[i].size(); j++) {
            printf("%d ", bcc[i][j]);
        }
        puts("");
    }
    return 0;
}