應用:線性時間內求出無向圖的割點與橋,雙連通分量。有向圖的強連通分量,必經點和必經邊。
主要是求兩個東西,dfn和low
時間戳dfn:就是dfs序,也就是每個節點在dfs遍歷的過程中第一次被訪問的時間順序。
追溯值low:$low[x]$定義為$min(dfn[subtree(x)中的節點], dfn[通過1條不再搜索樹上的邊能到達subtree(x)的節點])$,其中$subtree(x)$是搜索樹中以$x$為根的節點。
其實這個值表示的就是這個點所在子樹的最先被訪問到的節點,作為這個子樹的根。
搜索樹:在無向連通圖中任選一個節點出發進行深度搜索遍歷,每個點只訪問一次,所有發生遞歸的邊$(x,y)$構成一棵樹,稱為無向連通圖的搜索樹。
low計算方法:
先令$low[x] = dfn[x]$, 考慮從$x$出發的每條邊$(x,y)$
若在搜索樹上$x$是$y$的父節點,令$low[x]=min(low[x], low[y])$
若無向邊$(x,y)$不是搜索樹上的邊,則令$low[x] = min(low[x], dfn[y])$
割邊判定法則:
無向邊$(x,y)$是橋,當且僅當搜索樹上存在$x$的一個子節點$y$,滿足:$dfn[x] < low[y]$
這說明從$subtree(y)$出發,在不經過$(x,y)$的前提下,不管走哪條邊都無法到達$x$或比$x$更早訪問的節點。若把$(x,y)$刪除,$subtree(y)$就形成了一個封閉的環境。
橋一定是搜索樹中的邊,並且一個簡單環中的邊一定不是橋。
1 void tarjan(int x, int in_edge) 2 { 3 dfn[x] = low[x] = ++num; 4 int flag = 0; 5 for(int i = head[x]; i; i = Next[i]){ 6 int y = ver[i]; 7 if(!dfn[y]){ 8 tarjan(y); 9 low[x] = min(low[x], low[y]); 10 if(low[y] > dfn[x]){ 11 bridge[i] = bridge[i ^ 1] = true; 12 } 13 } 14 else if(i != (in_edge ^ 1)) 15 low[x] = min(low[x], dfn[y]); 16 } 17 } 18 19 int main() 20 { 21 cin>>n>>m; 22 tot = 1; 23 for(int i = 1; i <= m; i++){ 24 int x, y; 25 scanf("%d%d", &x, &y); 26 if(x == y)continue; 27 add(x, y); 28 add(y, x); 29 } 30 for(int i = 1; i <= n; i++){ 31 if(!dfn[i]){ 32 tarjan(i, 0); 33 } 34 } 35 for(int i = 2; i < tot; i += 2){ 36 if(bridge[i]) 37 printf("%d %d\n", ver[i ^ 1], ver[i]); 38 } 39 }
割點判定法則:
若$x$不是搜索樹的根節點,則$x$是割點當且僅當搜索樹上存在$x$的一個子節點$y$,滿足:$dfn[x]\leq low[y]$
特別地,若$x$是搜索樹地根節點,則$x$是割點當且僅當搜索樹上存在至少兩個子節點$y_1,y_2$滿足上述條件。
1 #include<cstdio> 2 #include<cstdlib> 3 #include<map> 4 #include<set> 5 #include<cstring> 6 #include<algorithm> 7 #include<vector> 8 #include<cmath> 9 #include<stack> 10 #include<queue> 11 #include<iostream> 12 13 #define inf 0x7fffffff 14 using namespace std; 15 typedef long long LL; 16 typedef pair<int, int> pr; 17 18 const int SIZE = 100010; 19 int head[SIZE], ver[SIZE * 2], Next[SIZE * 2]; 20 int dfn[SIZE], low[SIZE], n, m, tot, num; 21 bool bridge[SIZE * 2]; 22 23 void add(int x, int y) 24 { 25 ver[++tot] = y, Next[tot] = head[x], head[x] = tot; 26 } 27 28 void tarjan(int x) 29 { 30 dfn[x] = low[x] = ++num; 31 int flag = 0; 32 for(int i = head[x]; i; i = Next[i]){ 33 int y = ver[i]; 34 if(!dfn[y]){ 35 tarjan(y); 36 low[x] = min(low[x], low[y]); 37 if(low[y] >= dfn[x]){ 38 flag++; 39 if(x != root || flag > 1)cut[x] = true; 40 } 41 } 42 else low[x] = min(low[x], dfn[y]); 43 } 44 } 45 46 int main() 47 { 48 cin>>n>>m; 49 tot = 1; 50 for(int i = 1; i <= m; i++){ 51 int x, y; 52 scanf("%d%d", &x, &y); 53 if(x == y)continue; 54 add(x, y); 55 add(y, x); 56 } 57 for(int i = 1; i <= n; i++){ 58 if(!dfn[i]){ 59 root = i; 60 tarjan(i); 61 } 62 } 63 for(int i = 1; i <= n; i++){ 64 if(cut[i])printf("%d", i); 65 } 66 puts("are cut-vertexes"); 67 }
雙連通分量
若一張無向連通圖不存在割點,則稱它為“點雙連通圖”。若一張無向連通圖不存在橋,則稱他為“邊雙連通圖”。
無向圖的極大點雙連通子圖被稱為“點雙連通分量”,簡記為v-DCC。無向連通圖的極大邊雙連通子圖被稱為“邊雙連通分量”,簡記為e-DCC。
定理1一張無向連通圖是點雙連通圖,當且僅當滿足下列兩個條件之一:
1.圖的頂點數不超過2.
2.圖中任意兩點都同時包含在至少一個簡單環中。
定理2一張無向連通圖是邊雙連通圖,當且僅當任意一條邊都包含在至少一個簡單環中。
邊雙連通分量求法
求出無向圖中所有的橋,刪除橋后,每個連通塊就是一個邊雙連通分量。
用Tarjan標記所有的橋邊,然后對整個無向圖執行一次深度優先遍歷(不訪問橋邊),划分出每個連通塊。
1 int c[SIZE], dcc; 2 3 void dfs(int x){ 4 c[x] = dcc; 5 for(int i = head[x]; i; i = Next[i]){ 6 int y = ver[i]; 7 if(c[y] || bridge[i])continue; 8 dfs(y); 9 } 10 } 11 12 //main() 13 for(int i = 1; i <= n; i++){ 14 if(!c[i]){ 15 ++dcc; 16 dfs(i); 17 } 18 } 19 printf("There are %d e-DCCs.\n", dcc); 20 for(int i = 1; i <= n; i++){ 21 printf("%d belongs to DCC %d.\n", i, c[i]); 22 }
e-DCC的縮點
把e-DCC收縮為一個節點,構成一個新的樹,存儲在另一個鄰接表中。
1 int hc[SIZE], vc[SIZE * 2], nc[SIZE * 2], tc; 2 void add_c(int x, int y){ 3 vc[++tc] = y; 4 nc[tc] = hc[x]; 5 hc[x] = tc; 6 } 7 8 //main() 9 tc = 1; 10 for(int i = 2; i <= tot; i++){ 11 int x = ver[i ^ 1]; 12 y = ver[i]; 13 if(c[x] == c[y])continue; 14 add_c(c[x], c[y]); 15 } 16 printf("縮點之后的森林, 點數%d, 邊數%d(可能有重邊)\n", dcc, tc / 2); 17 for(int i = 2; i < tc; i++){ 18 printf("%d %d\n", vc[i ^ 1], vc[i]); 19 }
點雙連通分量的求法
橋不屬於任何e-DCC,割點可能屬於多個v-DCC
在Tarjan算法過程中維護一個棧,按照如下方法維護棧中的元素:
1.當一個節點第一次被訪問時,該節點入棧。
2.當割點判定方法則中的條件$dfn[x]\leq low[y]$成立時,無論$x$是否為根,都要:
(1)從棧頂不斷彈出節點,直至節點$y$被彈出
(2)剛才彈出的所有節點與節點$x$一起構成一個v-DCC
1 void tarjan(int x){ 2 dfn[x] = low[x] = ++num; 3 stack[++top] = x; 4 iff(x == root && head[x] == 0){ 5 dcc[++cnt].push_back(x); 6 return; 7 } 8 int flag = 0; 9 for(int i = head[x]; i; i = Next[i]){ 10 int y = ver[i]; 11 if(!dfn[y]){ 12 tarjan(y); 13 low[x] = min(low[x], low[y]); 14 if(low[y] >= dfn[x]){ 15 flag++; 16 if(x != root || flag > 1)cut[x] = true; 17 cnt++; 18 int z; 19 do{ 20 z = stack[top--]; 21 dcc[cnt].push_back(z); 22 23 }while(z != y); 24 dcc[cnt].push_back(x); 25 } 26 } 27 else low[x] = min(low[x], dfn[y]); 28 } 29 } 30 31 //main() 32 for(int i = 1; i <= cnt; i++){ 33 printf("e-DCC #%d:", i); 34 for(int j = 0; j < dcc[i].size(); j++){ 35 printf(" %d", dcc[i][j]); 36 } 37 puts(""); 38 }
v-DCC的縮點
設圖中共有$p$個割點和$t$個v-DCC,新圖將包含$p+t$個節點。
1 //main 2 num = cnt; 3 for(int i = 1; i <= n; i++){ 4 if(cnt[i])new_id[i] = ++num; 5 } 6 tc = 1; 7 for(int i = 1; i <= cnt; i++){ 8 for(int j = 0; j < dcc[i].size(); j++){ 9 int x = dcc[i][j]; 10 if(cut[x]){ 11 add_c(i, new_id[x]); 12 add_c(new_id[x], i); 13 } 14 else c[x] = i; 15 } 16 } 17 printf("縮點之后的森林, 點數%d, 邊數%d\n", num, tc / 2); 18 printf("編號1~%d的為原圖的v-DCC, 編號>%d的為原圖割點\n", cnt, cnt); 19 for(int i = 2; i < tc; i += 2){ 20 printf("%d %d\n", vc[i ^ 1], vc[i]); 21 }
有向圖的強連通分量
一張有向圖,若對於圖中任意兩個節點$x,y$,既存在$x$到$y$的路徑,也存在$y$到$x$的路徑,則稱該有向圖是強連通圖。
有向圖的極大強連通子圖被稱為強連通分量,簡記為SCC。
一個環一定是強連通圖,Tarjan算法的基本思路就是對每個點,盡量找到與它能構成環的所有節點。
Tarjan在深度優先遍歷的同時維護了一個棧,當訪問到節點$x$時,棧中需要保存一下兩類節點:
1.搜索樹上$x$的祖先節點,記為$anc(x)$
2.已經訪問過,並且存在一條路徑到達$anc(x)$的節點
實際上棧中的節點就是能與從$x$出發的“后向邊”和“橫叉邊”形成環的節點。
追溯值:
定義為滿足一下條件的節點的最小時間戳:
1.該點在棧中。
2.存在一條存subtree(x)出發的有向邊,以該點為終點。
計算步驟:
1.當節點$x$第一次被訪問時,把$x$入棧,初始化$low[x]=dfn[x]$
2.掃描從$x$出發的每條邊$(x,y)$
(1)若$y$沒被訪問過,則說明$(x,y)$是樹枝邊,遞歸訪問$y$,從$y$回溯后,令$low[x] = min(low[x], low[y])$
(2)若$y$被訪問過且$y$在棧中,令$low[x] = min(low[x], dfn[y])$
3.從$x$回溯之前,判斷是否有$low[x] = dfn[x]$。若成立,則不斷從棧中彈出節點直至$x$出棧。
強連通分量判定法則
追溯值計算過程中,若從$x$回溯前,有$low[x] = dfn[x]$成立,則棧中從$x$到棧頂的所有節點構成一個強連通分量。
如果$low[x]=dfn[x]$,說明$subtree(x)$中的節點不能與棧中其他節點一起構成環。另外,因為橫叉邊的終點時間戳必定小於起點時間戳,所以$subtree(x)$中的節點也不可能直接到達尚未訪問的節點(時間戳更大)
1 const int N = 100010, M = 1000010; 2 int ver[M], Next[M], head[N], dfn[N], low[N]; 3 int stack[N], ins[N], c[N]; 4 vector<int>scc[N]; 5 int n, m, tot, num, top, cnt; 6 7 void add(int x, int y){ 8 ver[++tot] = y, Next[tot] = head[x], head[x] = tot; 9 } 10 11 void tarjan(int x){ 12 dfn[x] = low[x] = ++num; 13 stack[++top] = x, ins[x] - 1; 14 for(int i = head[x]; i; i = Next[i]){ 15 if(!dfn[ver[i]]){ 16 tarjan(ver[i]); 17 low[x] = min(low[x], low[ver[i]]); 18 }else if(ins[ver[i]]){ 19 low[x] = min(low[x], dfn[ver[i]]); 20 } 21 } 22 if(dfn[x] == low[x]){ 23 cnt++; 24 int y; 25 do{ 26 y = stack[top--], ins[y] = 0; 27 c[y] = cnt, scc[cnt].push_back(y); 28 }while(x != y); 29 } 30 } 31 32 int main(){ 33 cin>>n>>m; 34 for(int i = 1; i <= m; i++){ 35 int x, y; 36 scanf("%d%d", &x, &y); 37 add(x, y); 38 } 39 for(int i = 1; i <= n; i++){ 40 if(!dfn[i])tarjan(i); 41 } 42 }
縮點
1 void add_c(int x, int y){ 2 vc[++tc] = y, nc[tc] = hc[x], hc[x] = tc; 3 } 4 5 //main 6 for(int x = 1; x <= n; x++){ 7 for(int i = head[x]; i; i = Next[i]){ 8 int y = ver[i]; 9 if(c[x] == c[y])continue; 10 add_c(c[x], c[y]); 11 } 12 }
李煜東的《圖連通性若干擴展問題探討》,有點難。