关于 Tarjan 算法

§在无向图中

Tarjan 可以求割点/桥,求点双/边双。

我们将图中的边分为「树边」和「非树边」。

定义 $dfn[i]$ 为到达 $i$ 的时间戳,$low[x]$ 表示以 $x$ 为根的子树中所有点只走一条边非树边能到达的最小时间。

无向图中求 $dfn$ 和 $low$ 的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int dfn[N], low[N], Time;
void dfs(int x, int frm) {
    dfn[x] = low[x] = ++Time;
    for (node i : edge[x]) {
        if (!dfn[i.to]) {
            dfs(i.to, i.num);
            low[x] = min(low[x], low[i.to]);
        } else if (i.num != frm)
            low[x] = min(low[x], dfn[i.to]);
    }
}

§

如果一个点儿子的 $low[to]>dfn[x]$ ,就可以说明 $x\to to$ 的边为割边。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <bits/stdc++.h>
#define pii pair<int, int>
using namespace std;
const int N = 160;
struct node {int to, num;};
vector<node> edge[N];
vector<pii> ans;
int dfn[N], low[N], n, m, cnt, Time;
void dfs(int x, int frm) {
    dfn[x] = low[x] = ++Time;
    for (node i : edge[x]) {
        if (!dfn[i.to]) {
            dfs(i.to, i.num);
            low[x] = min(low[x], low[i.to]);
            if (low[i.to] > dfn[x])
                ans.push_back({ x, i.to });
        } else if (i.num != frm)
            low[x] = min(low[x], dfn[i.to]);
    }
}
int main() {
    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int x, y;
        cin >> x >> y;
        edge[x].push_back({ y, ++cnt });//将边编号
        edge[y].push_back({ x, cnt });
    }
    dfs(1, 0);
    sort(ans.begin(), ans.end());
    for (pii i : ans) cout << i.first << " " << i.second << '\n';
}

§边双联通分量

先求出割边并标记,然后不经过割边跑DFS即可。

代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+100;
int dfn[N],low[N],n,m,cnt,Time,mark[N*10],col,vis[N];
struct node{int to,num;};
vector<node>edge[N];
vector<int>ans[N];
void tarjan(int x,int frm){
    dfn[x]=low[x]=++Time;
    for(node i:edge[x]){
        if(!dfn[i.to]){
            tarjan(i.to,i.num);
            low[x]=min(low[x],low[i.to]);
            if(low[i.to]>dfn[x]) mark[i.num]=1;
        }else if(i.num!=frm) low[x]=min(low[x],dfn[i.to]);
    }
}
void dfs(int x){
    if(vis[x]) return;
    vis[x]=1;
    ans[col].push_back(x);
    for(node i:edge[x])if(mark[i.num]==0) dfs(i.to);
}
int main(){
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        int x,y;cin>>x>>y;
        edge[x].push_back({y,++cnt});
        edge[y].push_back({x,cnt});
    }
    for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i,0);
    for(int i=1;i<=n;i++) if(!vis[i]) col++,dfs(i);
    cout<<col<<"\n";
    for(int i=1;i<=col;i++){
        cout<<ans[i].size()<<" ";
        for(int j:ans[i]) cout<<j<<" ";
        cout<<'\n';
    }
}

§割点

对于根节点,如果他有多于两个儿子,则根节点为割点。

否则:如果一个点有儿子的 $low[to]\ge dfn[x]$ ,那么 $x$ 为割点。

代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include<bits/stdc++.h>
#define pii pair<int,int>
using namespace std;
const int N=2e4+10;
struct node{int to,num;};
vector<node>edge[N];
bool ans[N];
int dfn[N],low[N],n,m,cnt,Time;
void dfs(int x,int fa){
    int cnt=0;
    dfn[x]=low[x]=++Time;
    for(node i:edge[x]){
        if(!dfn[i.to]){
            dfs(i.to,x);cnt++;
            low[x]=min(low[x],low[i.to]);
            if(low[i.to]>=dfn[x]&&fa!=0) ans[x]=1;
        }else if(i.to!=fa) low[x]=min(low[x],dfn[i.to]);
    }
    if(!fa&&cnt>=2) ans[x]=1;
}
int main(){
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        int x,y;cin>>x>>y;
        edge[x].push_back({y,++cnt});
        edge[y].push_back({x,cnt});
    }
    for(int i=1;i<=n;i++) if(!dfn[i]) dfs(i,0);
    int cnt=0;
    for(int i=1;i<=n;i++) if(ans[i]) cnt++;
    cout<<cnt<<'\n';
    for(int i=1;i<=n;i++) if(ans[i]) cout<<i<<" ";
}

§点双联通分量

对于一个点双,它在 DFS 搜索树中 $dfn$ 值最小的点一定是割点或者树根。

我们用栈维护点,当遍历到某个点时将其入栈。

当某个点是割点或根时,就弹栈直到其儿子被弹出,弹出的这些点都与这个割点或根属于一个点双。

注意特判:根是一个孤立的点没有儿子时,自己是一个点双。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
int dfn[N],low[N],n,m,cnt,Time,mark[N],col,vis[N];
struct node{int to,num;};
vector<node>edge[N];
vector<int>ans[N];
stack<int>s;
void tarjan(int x,int frm){
    dfn[x]=low[x]=++Time;s.push(x);
    for(node i:edge[x]){
        if(!dfn[i.to]){
            tarjan(i.to,i.num);
            low[x]=min(low[x],low[i.to]);
            if(low[i.to]>=dfn[x]){//是割点或根
                col++;
                while(s.top()!=i.to){//弹栈直到儿子
                    ans[col].push_back(s.top());
                    s.pop();
                }
                ans[col].push_back(s.top());
                s.pop();//将儿子弹栈,且加入点双
                ans[col].push_back(x);//割点也属于点双
            }
        }else if(i.num!=frm) low[x]=min(low[x],dfn[i.to]);
    }
    if(!edge[x].size()&&!frm) ans[++col].push_back(x),s.pop();
}
int main(){
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        int x,y;cin>>x>>y;
        if(x==y) continue;
        edge[x].push_back({y,++cnt});
        edge[y].push_back({x,cnt});
    }
    for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i,0);
    cout<<col<<"\n";
    for(int i=1;i<=col;i++){
        cout<<ans[i].size()<<" ";
        for(int j:ans[i]) cout<<j<<" ";
        cout<<'\n';
    }
}

§在有向图中

Tarjan 用来求强联通分量。

有向图的 DFS 生成树主要有 4 种边(不一定全部出现):

  1. 树边(tree edge):示意图中以黑色边表示,每次搜索找到一个还没有访问过的结点的时候就形成了一条树边。
  2. 反祖边(back edge):示意图中以红色边表示(即 $7\to1$),也被叫做回边,即指向祖先结点的边。
  3. 横叉边(cross edge):示意图中以蓝色边表示(即 $9\to7$),它主要是在搜索的时候遇到了一个已经访问过的结点,但是这个结点 并不是 当前结点的祖先。
  4. 前向边(forward edge):示意图中以绿色边表示(即 $3\to6$),它是在搜索的时候遇到子树中的结点的时候形成的。

横叉边无效,且干扰算法,通过判断点是否在栈中,判掉。

对于一个连通分量图,在该连通图中有且仅有一个点使得 $dfn[x]=low[x]$ 。该结点一定是在深度遍历的过程中,该连通分量中第一个被访问过的结点,因为它的 $dfn$ 和 $low$ 值最小,不会被该连通分量中的其他结点所影响。

所以,我们每遇见一个 $dfn[x]=low[x]$ 的点,就弹栈直到自己被弹出,这些联通的点构成一个强联通分量。

代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
int dfn[N],low[N],n,m,cnt,Time,mark[N],vis[N],bel[N];
vector<int>edge[N];
stack<int>s;
void tarjan(int x){
    dfn[x]=low[x]=++Time;
    s.push(x);vis[x]=1;
    for(int to:edge[x]){
        if(!dfn[to]){
            tarjan(to);
            low[x]=min(low[x],low[to]);
        }else if(vis[to])/*vis为是否在栈中*/ low[x]=min(low[x],dfn[to]);
    }
    if(low[x]==dfn[x]){
        cnt++;
        while(s.top()!=x){//弹栈直到自己被弹出
            vis[s.top()]=0;
            bel[s.top()]=cnt;
            s.pop();
        }
        vis[x]=0;
        bel[x]=cnt;
        s.pop();
    }
}
int main(){
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        int x,y;cin>>x>>y;
        edge[x].push_back(y);
    }
    for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);
}