在介绍算法之前先引入几个概念:

时间戳: 在深度优先搜索时,每个点 x x x第一次被访问的顺序为时间戳,记作 d f n [ x ] dfn[x] dfn[x](从 1 1 1开始)

追溯值: x x x开始走,所能遍历到的最小时间戳,记作 l o w [ x ] low[x] low[x]

1. tarjan 1.\text {tarjan} 1.tarjan SCC \text{SCC} SCC

给定一张有向图 G G G ∀ u , v \forall u,v u,v,即存在从 u u u v v v的路径又存在从 v v v u u u的路径,那么该图 G G G是强连通图。

强连通分量 (SCC) \text{(SCC)} (SCC) 就是有向图的极大强连通子图

为了方便,我们定义有向图中的四种边:

( 1 ) (1) (1) 树枝边:搜索树中的边

( 2 ) (2) (2) 前向边:搜索树中 x x x y y y的祖先节点

( 3 ) (3) (3) 后向边:搜索树中 y y y x x x的祖先节点

( 4 ) (4) (4) 横叉边:以上三种情况之外的边: x x x y y y的边,并且满足 d f n [ y ] < d f n [ x ] dfn[y]<dfn[x] dfn[y]<dfn[x]

一定是往左边连的边,如果往右,那么根据深度优先遍历会走到右边。

其中后向边和横叉边是构成环的重要的边,所以为了找到这两条边, tarjan \text{tarjan} tarjan算法在深度优先搜索时维护一个来记录节点 x x x的祖先节点。如果正在访问的点恰好是栈中的祖先节点,那么就可以形成环,如果正在访问的点可以到栈中的祖先节点,那么他们三个以及搜索路径就可以构成环。

tarjan \text{tarjan} tarjan算法的步骤:

记全局时间戳为 t i m e s t a m p timestamp timestamp

( 1 ) (1) (1) x x x第一次被遍历时,将 x x x入栈,初始化 d f n [ x ] = l o w [ x ] = + + t i m e s t a m p dfn[x]=low[x]=++timestamp dfn[x]=low[x]=++timestamp

( 2 ) (2) (2) 遍历 x x x的出边 j j j

① ① :若 j j j未被访问 ( d f n [ j ] = 0 ) (dfn[j]=0) (dfn[j]=0),那就访问 j j j,回溯之后更新追溯值 l o w [ x ] = min ⁡ ( l o w [ x ] , d f n [ j ] ) low[x]=\min(low[x], dfn[j]) low[x]=min(low[x],dfn[j])

② ② :若 j j j被访问过且 j j j在栈中,更新追溯值 l o w [ x ] = min ⁡ ( l o w [ x ] , d f n [ j ] ) low[x]=\min(low[x],dfn[j]) low[x]=min(low[x],dfn[j])

( 3 ) (3) (3) x x x回溯之前,判断是否 l o w [ x ] = d f n [ x ] low[x]=dfn[x] low[x]=dfn[x],如果有那么说明是环,则不断将栈中节点出栈,直到 x x x出栈为止。

SCC \text{SCC} SCC的判定:

如果 x x x是强连通分量的最高点,等价于 l o w [ x ] = d f n [ x ] low[x]=dfn[x] low[x]=dfn[x],那么就说明找到了整个强连通分量,则栈中从 x x x到栈顶的所有节点构成强连通分量。

SCC \text{SCC} SCC缩点:

重新建图的时候只需要判定 i d [ i ] id[i] id[i]是否等于 i d [ j ] id[j] id[j]
如果不相同就建新边。

代码:

void tarjan(int x) {
   
    dfn[x] = low[x] = ++ timestamp;
    s.push(x), st[x] = 1;
    for (int i = h[x]; ~i; i = ne[i]) {
   
        int j = e[i];
        if (!dfn[j]) {
   
            tarjan(j);
            low[x] = min(low[x], low[j]);
        } else if (st[j]) {
   
            low[x] = min(low[x], dfn[j]);
        }
    }
    if (dfn[x] == low[x]) {
   
        scc_cnt ++;
        int y;
        do {
   
            y = s.top();
            s.pop();
            st[y] = 0;
            id[y] = scc_cnt;
            Size[scc_cnt] ++;
        } while (y != x);
    }
}

s c c c n t scccnt scccnt表示图中强连通分量的个数, i d [ i ] id[i] id[i]数组表示 x x x节点属于编号为 i i i的强连通分量, S i z e [ i ] Size[i] Size[i]数组表示编号为 i i i的强连通分量里点的个数。

2. tarjan 2.\text {tarjan} 2.tarjan e-DCC \text{e-DCC} e-DCC

给定一张无向图 G G G,如果不存在割边,则称为边双连通图,无向图的极大边双连通分量被称为边双连通分量 ( e-DCC ) (\text{e-DCC}) (e-DCC)

求割边:

找到一个条边,满足 d f n [ x ] < l o w [ y ] ( y 是 x 子 节 点 ) dfn[x] <low[y] (y是x子节点) dfn[x]<low[y](yx)
因为从 y y y出发,无论走到哪条边,都不可能到达 ≥ d f n [ x ] \ge dfn[x] dfn[x]的点,所以删除这条边之后就将图分成了两部分。在算法中,可以巧妙地运用异或运算来标记割边。

e-DCC \text{e-DCC} e-DCC的判定:

只需要求出无向图中的所有割边,把割边删除,无向图会分成若干连通块,每一个连通块就是一个边双连通分量。

e-DCC \text{e-DCC} e-DCC缩点:

把每个 e-DCC \text{e-DCC} e-DCC看成点,每条割边看成新边。

代码:

void tarjan(int x, int fa) {
   
    dfn[x] = low[x] = ++ timestamp;
    s.push(x);
    for (int i = h[x]; ~i; i = ne[i]) {
   
        int j = e[i];
        if (!dfn[j]) {
   
            tarjan(j, i);
            low[x] = min(low[x], low[j]);
            if (dfn[x] < low[j]) {
   
                st[i] = st[i ^ 1] = 1;
            }
        } else if (i != (fa ^ 1)) {
   
            low[x] = min(low[x], dfn[j]);
        }
    }
    if (low[x] == dfn[x]) {
   
        dcc_cnt ++;
        int y;
        do {
   
            y = s.top();
            s.pop();
            id[y] = dcc_cnt;
        } while (y != x);
    }
}

i d [ i ] id[i] id[i]表示点 y y y属于编号为i的双连通分量, s t [ i ] , s t [ i ∧ 1 ] st[i],st[i \wedge 1] st[i],st[i1]标记每条割边, d c c c n t dcccnt dcccnt表示边双连通分量的个数。

3. tarjan 3.\text {tarjan} 3.tarjan v-DCC \text{v-DCC} v-DCC

给定一张无向图 G G G,如果不存在割点,则称为点双连通图,无向图的极大点双连通分量被称为<stron> ( v-DCC ) (\text{v-DCC}) (v-DCC)</stron>

求割点:

如果 x x x不是搜索树的根节点,则 x x x是割点当且仅当搜索树上存在一个子节点 y y y使得 d f n [ x ] ≤ l o w [ y ] dfn[x] \le low[y] dfn[x]low[y]

v-DCC \text{v-DCC} v-DCC的判定:

特判孤立点为单独的点双连通分量,除此之外,其他的点双连通分量大小至少为 2 2 2
做法:
( 1 ) (1) (1) 当节点第一次被访问,将节点入栈。
( 2 ) (2) (2) 当点 x x x,出边 y y y满足 d f n [ x ] ≤ l o w [ y ] dfn[x] \le low[y] dfn[x]low[y], 从栈顶不断弹出节点,直到 y y y被弹出,那么所有弹出的节点与点 x x x构成一个点双连通分量。

v-DCC \text{v-DCC} v-DCC缩点:

由于一个割点可能属于多个 v-DCC \text{v-DCC} v-DCC,所以建图时必须将割点单独拿出来建,即每个割点向该割点所在的 v-DCC \text{v-DCC} v-DCC连一条新边。设无向图 G G G p p p个割点和 t t t v-DCC \text{v-DCC} v-DCC,那么新图的节点个数为 p + t p + t p+t

代码:

void tarjan(int x) {
   
    dfn[x] = low[x] = ++ timestamp;
    s.push(x);
    if (x == root && h[x] == -1) {
   
        dcc_cnt ++;
        dcc[dcc_cnt].push_back(x);
        return ;
    }
    int cnt = 0;
    for (int i = h[x]; ~i; i = ne[i]) {
   
        int j = e[i];
        if (!dfn[j]) {
   
            tarjan(j);
            low[x] = min(low[x], low[j]);
            if (dfn[x] <= low[j]) {
   
                cnt ++;
                if (x != root || cnt > 1) st[x] = 1;
                dcc_cnt ++;
                int y;
                do {
   
                    y = s.top();
                    s.pop();
                    dcc[dcc_cnt].push_back(y);
                } while (y != j);
                dcc[dcc_cnt].push_back(x);
            } 
        } else {
   
            low[x] = min(low[x], dfn[j]);
        }
    }
}

d c c c n t dcccnt dcccnt表示点双连通分量的个数, d c c [ i ] [ j ] dcc[i][j] dcc[i][j]数组表示新图中割点编号为 i i i,对应 v-DCC \text{v-DCC} v-DCC中编号为 j j j的邻接矩阵。