[雙連通分量] tarjan演算法
在前面說的話
其實這個借鑑了網上的一些教程/總結,但是主要還是看lrj的藍書並有部分引用以及自己的一些理解而成的,僅僅是為了給自己或他人總結用的,而不希望用於任何其他的用途亦或是被說抄襲。
引入
首先先來看幾個概念
割點(割頂、關節點),在一個無向圖中,如果刪除了某一個點,能使連通分量的個數增加, 那麼稱這個點為割點
特殊的情況:對於一個連通圖,割點就是在刪除以後能讓原圖不再連通的點
橋,在一個無向圖中,刪除了一條邊,能使連通分量個數增加,則這條邊為橋
那麼首先先來看看如何在一個無向圖中求出割點和橋。
暴力
暴力顯然是首先想到的,在一個無向圖中,嘗試刪除每一個點(邊),然後做一次dfs檢查連通分量的個數是否增加。
但很顯然這種辦法很慢,預計的時間複雜度應為
lrj藍書上的基於DFS的做法
其實這種做法並不一定是lrj首創,但在這裡也沒有去找原作者,而lrj也沒有提,或許是人人皆知的做法吧(其實也可能是tarjan演算法的基礎)
首先這個演算法需要時間戳,於是這裡用
那麼DFS會產生一個DFS樹,在這棵樹中,又有幾個概念要認識
首先是樹邊,就是DFS樹上的邊,但是除了樹邊還會有反向邊,即由後代連回祖先的邊。
那麼考慮出現什麼情況才能證明是割點呢。先考慮一棵樹的樹根,顯然,當樹根下有多個子樹時,它會是割點,但當只有一棵子樹時,它不是割點。
然後考慮下面的點,這裡有定理:
非根節點
很顯然,如果
幹說不好懂,那麼上圖。
那麼顯然地,如果在2333
試想,如果連回了
那麼如果用
那麼這個定理可以簡寫為當
那麼在這裡其實還有一種特殊情況,當
程式碼還是比較容易寫的,而且在後面求雙連通分量的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;
}