图论·双连通分量

注:双连通分量是针对无向图的概念。

对于一个连通图,如果任意两点至少存在两条“点不重复”的路径,则说这个图是点-双连通的(双连通)。这个要求等价于任意两条边都在同一个简单环中,即内部无割顶。类似地,如果任意两点至少存在两条“边不重复”的路径,我们说这个图是边-双连通的,要求每条边都至少在一个简单环中,即所有边都不是桥。显然边双连通相对点双连通是一个更弱的条件。对于一张无向图,点-双连通的极大子图称为双连通分量(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时不经过桥即可。

posted @ 2016-08-10 17:54  astoninfer  阅读(369)  评论(0编辑  收藏  举报