连通性2 无向图的割边 (cut edge)
这是DFS系列的第二篇
In graph theory, a bridge, isthmus, cut-edge, or cut arc is an edge of a graph whose deletion increases its number of connected components. Equivalently, an edge is a bridge if and only if it is not contained in any cycle. A graph is said to be bridgeless or isthmus-free if it contains no bridges.
Let $G = (V, E)$ be a connected, undirected graph, a bridge of $G$ is an edge whose removal disconnects $G$. (Introduction to Algorithms p.621)
注意:割边这一概念只适用于无向图,不适用于有向图,因为有向图的连通性和无向图的连通性是完全不同的两个概念。对于某有向图 $G$,简单地称它连通是很不完善的。有向图的连通性有强连通(strongly connected)和半连通(semiconnected)两种常见的提法。上面英文描述中的“graph”及下文中的“图”均指无向图。
割边 (cut edge)也称作桥(bridge)是删除后能使图的连通分量增加的边。
下面我们只考虑没有重边的无向图。
考虑一个连通的无向图 $G$,若它含有某条割边 $(u, v)$,那么去掉这条边后,将得到2个连通图 $G'$,$H'$,而不可能得到 $2$ 个以上连通图,因为一条边最多能将 $2$ 个连通图合为一个联通图。(这句话貌似和上下文无关)
下面介绍求无向图所有割边的Tarjan算法(Tarjan's Bridge-Finding Algorithm)
我们只考虑对无向连通图 $G$ 求割边,若图 $G$ 不连通那么就对 $G$ 的各个连通分量求割边。
我们知道 DFS 一个无向图将其所有边分成两类,树边(tree edge)与回边(back edge)。显然地,割边只能是树边而绝不可能是回边。
考虑 一条树边 $(u\to v)$ 是割边 的条件。这条件应当是在DFS树中,以 $v$ 为根的子树(简称子树 $v$)中的所有节点都没有连向 $u$ 的祖先节点(包括 $u$ 本身)的回边,也就是说子树 $v$ 仅仅靠着边 $(u,v)$ 和其他节点保持连通。
为了判断上述条件,我们在 DFS 过程中记录每个节点的 dfn 值与 low 值,树边 $(u\to v)$ 是割边的充要条件即是 \(\color{blue}{\mathrm{low}[v]>\mathrm{dfn}[u]}\) 。
struct edge{
int to, nt;
bool flag;
}E[MAX_E<<1];
int head[MAX_V];
int dfn[MAX_V], low[MAX_V];
int ts; //time stamp
void dfs(int u, int f){
dfs[u]=low[u]=++ts;
for(int i=head[i]; ~i; i=E[i].nt){
int &v=E[i].to;
if(!dfn[v]){ //tree edge
dfs(v, f);
low[u]=min(low[u], low[v]);
if(low[v]>dfn[u]){
e[i].flag=e[i^1].flag=true;
}
}
else if(v!=f&&dfn[v]<dfn[u]){ //back edge
low[u]=min(low[u], dfn[v]);
}
}
}
void solve(int N){
memset(dfn, 0, sizeof(dfn));
ts=0;
for(int i=1; i<=N; i++)
if(!dfn[i]) dfs(i, i);
}
现在考虑有重边的情况。这时上面的写法不能识别所有回边。首先明确一点:不论是否有重边,DFS 都将所有边分成树边与回边两类。
但是按上面的写法,所有重边要么全是树边,要么全是回边,因而不能识别所有回边(这并不是 DFS 算法本身有问题,而是写法有问题)。这是因为 DFS 的参数是 $u$(当前节点)和 $f$(当前节点的父亲节点),我们判断回边的依据是
else if(v!=f&&dfn[v]<dfn[u]){ //back edge
low[u]=min(low[u], dfn[v]);
}
解决办法是将参数 $u$换成树边 $( u\to f )$ 的编号。
struct edge{
int to, nt, id;
bool tag;
}E[MAX_N<<1];
int head[MAX_N];
int dfn[MAX_N], low[MAX_N], ts; //time_stamp
void dfs(int u, int te){
dfn[u]=low[u]=++ts;
for(int i=head[u]; ~i; i=E[i].nt){
int &v=E[i].to, &id=E[i].id;
if(!dfn[v]){ //tree_edge
dfs(v, id);
low[u]=min(low[u], low[v]);
if(low[v]>dfn[u])
e[i].tag=true;
}
else if(id!=te&&dfn[v]<dfn[u]){ //back_edge
low[u]=min(low[u], dfn[v]);
}
}
}
一般来说,没必要在结构体 edge 内加个变量 id,按通常的建图方式,无向边的 ID 就是其对应的某条有向边的 ID 右移一位。