注:双连通分量是针对无向图的概念。
对于一个连通图,如果任意两点至少存在两条“点不重复”的路径,则说这个图是点-双连通的(双连通)。这个要求等价于任意两条边都在同一个简单环中,即内部无割顶。类似地,如果任意两点至少存在两条“边不重复”的路径,我们说这个图是边-双连通的,要求每条边都至少在一个简单环中,即所有边都不是桥。显然边双连通相对点双连通是一个更弱的条件。对于一张无向图,点-双连通的极大子图称为双连通分量(bcc)或块。不难看出,每条边恰好属于一个双连通分量,但不同双连通分量可能会有公共点。实际上,可以证明不同双连通分量最多只有一个公共点,且它一定是割顶。同时,任意割顶都是至少两个不同双连通分量的公共点。
首先了解一下无向图的割顶(cut vertex)与桥(bridge)。它们是保持连通分量连通性的关键结点或边,也就是说删除某个连通分量的割顶之后,原图不再连通,桥的判定方法类似。可以在线性时间内找到一张无向连通图的所有割顶。我们考虑对于一张无向连通图$G$进行dfs得到的dfs树,显然树根是割顶当且仅当它有两个或以上的子树,对于非树根结点有如下性质:
在无向连通图$G$的dfs树中,非根结点$u$是$G$的割顶当且仅当$u$存在一个子结点$v$,使得以该子结点为根的子树中所有结点均没有反向边连回$u$的祖先。
方便起见,设$low(u)$为$u$及其后代所能连回的最早的祖先的$pre$值,则上述条件就可以简单地写成结点$u$存在一个子结点$v$,使得$low(v) geq pre(u)$。作为一种特殊情况,如果$v$的后代只能连回$v$自己(即$low(v) > pre(u) $),只需删除$(u, v)$一条边就可以让图$G$非连通,因此$(u, v)$是桥。下面给出判定图$G$中割顶的代码:
1 int dfs(int u, int fa){ 2 int lowu = pre[u] = ++dfs_clk; 3 int child = 0; 4 FOR(i, 0, G[u].size() - 1){//FOR(i, j, k) :: for(int i = j; i <= k; i++) 5 int v = G[u][i]; 6 if(!pre[v]){ // v never visited before 7 child++; 8 int lowv = dfs(v, u); 9 minimize(lowu, lowv);//min(x, y) :: x = min(x, y) 10 if(lowv >= pre[u]) is_cut[u] = 1; // u is vertex cut 11 }else if(pre[v] < pre[u] && v != fa) minimize(lowu, pre[v]); 12 } 13 if(fa < 0 && child == 1) is_cut[u] = 0; 14 low[u] = lowu; 15 return lowu; 16 }
$pre(u)$是结点$u$在dfs时第一次被访问的时间,对于从结点$u$发出的边,如果指向一个未访问过的结点,可以用子结点的$low[v]$来更新$low[u]$,否则如果$v$不是$u$的父结点,说明该边是一条反向边($v$在dfs树中的父结点并不是$u$),用该边更新$low[u]$。
计算点-双连通分量一般用如下算法(Tarjan):
1 vector<int> G[maxn], bcc[maxn]; 2 int pre[maxn], is_cut[maxn], bcc_no[maxn]; 3 int dfs_clk, bcc_cnt; 4 int odd[maxn], color[maxn]; 5 struct E{ 6 int u, v; 7 E(int u = 0, int v = 0) : u(u), v(v) {} 8 }; 9 stack<E> S; 10 int dfs(int u, int fa){ 11 int lowu = pre[u] = ++dfs_clk; 12 int ch = 0; 13 FOR(i, 0, G[u].size() - 1){ 14 int v = G[u][i]; 15 E e = E(u, v); 16 if(!pre[v]){ 17 S.push(e); 18 ch++; 19 int lowv = dfs(v, u); 20 minimize(lowu, lowv); 21 if(lowv >= pre[u]){ 22 is_cut[u] = 1; 23 bcc_cnt++, bcc[bcc_cnt].clear(); 24 while(true){ 25 E x = S.top(); S.pop(); 26 if(bcc_no[x.u] != bcc_cnt){ 27 bcc[bcc_cnt].pb(x.u), bcc_no[x.u] = bcc_cnt; 28 } 29 if(bcc_no[x.v] != bcc_cnt){ 30 bcc[bcc_cnt].pb(x.v), bcc_no[x.v] = bcc_cnt; 31 } 32 if(x.u == u && x.v == v) break; 33 } 34 } 35 }else if(pre[v] < pre[u] && v != fa){ 36 S.push(e); 37 minimize(lowu, pre[v]); 38 } 39 } 40 if(fa < 0 && ch == 1) is_cut[u] = 0; 41 return lowu; 42 } 43 44 void find_bcc(int n){ 45 clr(pre, 0), clr(is_cut, 0), clr(bcc_no, 0); 46 dfs_clk = bcc_cnt = 0; 47 FOR(i, 0, n - 1) if(!pre[i]) dfs(i, -1); 48 //post condition : S is empty 49 }
边-双连通分量可以用更简单的办法求出,分两个步骤,先做一次dfs标记出所有的桥,然后再做一次dfs找出边-双连通分量。因为边-双连通分量是没有公共结点的,所以只要在第二次dfs时不经过桥即可。