在前面说的话
其实这个借鉴了网上的一些教程/总结,但是主要还是看lrj的蓝书并有部分引用以及自己的一些理解而成的,仅仅是为了给自己或他人总结用的,而不希望用于任何其他的用途亦或是被说抄袭。
引入
首先先来看几个概念
割点(割顶、关节点),在一个无向图中,如果删除了某一个点,能使连通分量的个数增加, 那么称这个点为割点
特殊的情况:对于一个连通图,割点就是在删除以后能让原图不再连通的点
桥,在一个无向图中,删除了一条边,能使连通分量个数增加,则这条边为桥
那么首先先来看看如何在一个无向图中求出割点和桥。
暴力
暴力显然是首先想到的,在一个无向图中,尝试删除每一个点(边),然后做一次dfs检查连通分量的个数是否增加。
但很显然这种办法很慢,预计的时间复杂度应为
O(n(n+m))
lrj蓝书上的基于DFS的做法
其实这种做法并不一定是lrj首创,但在这里也没有去找原作者,而lrj也没有提,或许是人人皆知的做法吧(其实也可能是tarjan算法的基础)
首先这个算法需要时间戳,于是这里用
pre
记录第一次访问的时间戳
那么DFS会产生一个DFS树,在这棵树中,又有几个概念要认识
首先是树边,就是DFS树上的边,但是除了树边还会有反向边,即由后代连回祖先的边。
那么考虑出现什么情况才能证明是割点呢。先考虑一棵树的树根,显然,当树根下有多个子树时,它会是割点,但当只有一棵子树时,它不是割点。
然后考虑下面的点,这里有定理:
非根节点
u
是图
很显然,如果
干说不好懂,那么上图。
那么显然地,如果在2333
试想,如果连回了
u
的祖先,那么这棵dfs树,可以在
那么如果用
那么这个定理可以简写为当
u
存在一个子节点
那么在这里其实还有一种特殊情况,当
代码还是比较容易写的,而且在后面求双连通分量的tarjan算法的思路基本是一致的。
双连通分量
那么依旧,先来看看定义
什么是双连通分量呢?
就是说,在一个连通图中,任意两个点都能通过至少两条不经过同一个点的路径而互相到达,就说明它是点双连通的。
同样举几个栗子例子
我们暂且称这样的图叫铁三角好了23333
在这个图中1和2可以通过直接到达和走经过3的弯路而到达,1和3,2和3同理,所以这是一个点双连通的图,点双连通的判定条件比较复杂,这就涉及到我们的tarjan算法了,但是我们先放一放,看看边双连通。
如果是边双连通呢,那要求就更加简单,只要两条路径之间没有公共的边即可,就算经过同一个点也没关系。因此我们发现,只要一个图是点双连通的,就一定是边双连通的,那么判定就更加简单,只要是没有桥的一个图就好了。
那么双连通分量就是一个无向图中的双连通的一个极大子图,只要多画了几个图就会发现,割点总是多个点双连通分量的公共点,而边双连通分量则不会有公共点。
所以tarjan算法的基本思路就是,用找割点的办法, 凡是找到了一个割点,并且在这一棵子树会在这个点被去掉之后单独独立出来的,就是一个点双连通分量。因此这里用一个栈来保存访问过的边,直到发现了就不停地出栈,直到访问回这条边的时候就停止出栈,因为前面的边就是和祖先是一个双连通分量或是独立的了。那么代码和前面求割点的基本一致,只是在dfs处稍微多加了几句,还是比较好写的。
参考代码
#include <stack>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN = 1010,
MAXM = 100010;
struct Edge {
int u, v;
int ne;
} e[MAXM * 2];
int pre[MAXN], bcc_cnt, dfs_clock, bccno[MAXN];
// 用bcc_cnt来记录有多少个双连通分量 bccno则是每一个点处于哪一个双连通分量中,对于割点bccno无意义,储存的是最后一个双连通分量的编号
bool iscut[MAXN];
int head[MAXN];
vector bcc[MAXN];
stack S;
int dfs(int u, int fa) {
int lowu = pre[u] = ++dfs_clock;
int child = 0;
for(int i = head[u]; ~i; i = e[i].ne) {
int v = e[i].v;
if(!pre[v]) { // 未访问
S.push(e[i]);
child++;
int lowv = dfs(v, u);
lowu = min(lowu, lowv);
if(lowv >= pre[u]) { // 如果找到了双连通分量
iscut[u] = 1;
++bcc_cnt;
bcc[bcc_cnt].clear();
for(;;) { // 出栈
Edge x = S.top();
S.pop();
if(bccno[x.u] != bcc_cnt) {
bcc[bcc_cnt].push_back(x.u);
bccno[x.u] = bcc_cnt;
}
if(bccno[x.v] != bcc_cnt) {
bcc[bcc_cnt].push_back(x.v);
bccno[x.v] = bcc_cnt;
}
if(x.u == u && x.v == v) {
break;
}
}
}
} else {
if(pre[v] < pre[u] && v != fa) {
S.push(e[i]);
lowu = min(lowu, pre[v]); // 用反向边更新自己
}
}
}
if(fa < 0 && child == 1) {
iscut[u] = 0;
}
return lowu;
}
void find_bcc(int n) {
memset(pre, 0, sizeof pre);
memset(iscut, 0, sizeof iscut);
memset(bccno, 0, sizeof bccno);
dfs_clock = bcc_cnt = 0;
for(int i = 0; i < n; ++i) {
if(!pre[i]) {
dfs(i, -1);
}
}
}
int main(void) {
int n, m;
int cnt = 0;
memset(head, -1, sizeof head);
scanf("%d%d", &n, &m);
for(int i = 0; i < m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
e[cnt].u = u;
e[cnt].v = v;
e[cnt].ne = head[u];
head[u] = cnt++;
e[cnt].u = v;
e[cnt].v = u;
e[cnt].ne = head[v];
head[v] = cnt++;
}
find_bcc(n);
printf("%d\n", bcc_cnt);
for(int i = 1; i <= bcc_cnt; ++i) {
printf("%d : ", i);
for(int j = 0; j < bcc[i].size(); j++) {
printf("%d ", bcc[i][j]);
}
puts("");
}
return 0;
}