Tarjan算法【閱讀筆記】


應用:線性時間內求出無向圖的割點與橋,雙連通分量。有向圖的強連通分量,必經點和必經邊。

主要是求兩個東西,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 }

 

 

李煜東的《圖連通性若干擴展問題探討》,有點難。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM