给定一个无向图G(V, E),若对于e
∈
\in
∈E,从图中删除边 e 后,G 分裂成两个不相连的子图,则称 e 为 G 桥或割边。
OK,定义很简单。可以通过Tarjan算法求解。在Tarjan中,有两个数组dfn[ ] 和 low[ ] ,其中dfn 表示搜索树的搜索次序,low 记录的是通过不在搜索树上的边能够到达的dfn 的最小值,某一条边e (x和y之间的连边)是桥,当且仅当dfn[x] < low[y] (搜索树上x是y的父节点),这个不等式表明,在不通过非搜索树边上的边的情况下,无法到达一个更早的时间戳(即无法到达祖先节点),说明如果删除这条边,后续的节点就无法与前面的节点保持联系,因此属于割边。
但是需要注意两点
- 因为是无向图,搜索树上的子节点总可以到达父节点,因此可以在tarjan的参数中加入父节点的编号,对非父节点的边搜索即可。
- 可能会有重边,但是重边的话可以返回父节点,只是不通过来的边返回而已。此处继续%lydrainbow。在用邻接表保存无向图时候,通常我们会将同一条边(正向和反向)相邻的保存,因此不返回父节点其实就相当于不反回相邻的节点,因此可以用成对变换的技巧判断是否是同一条边。
拓展:成对变换
对于一个偶数
x
x
x,
x
x
x
x
o
r
xor
xor 1 =
x
x
x + 1
对于一个奇数
x
x
x,
x
x
x
x
o
r
xor
xor 1 =
x
x
x - 1
根据这个技巧,在邻接表中,可以将下标以(2,3),(4,5)这样的形式,成对存放,如果某一个点to[x] 是这条双向边的终点,那么to[x^1] 就是这个边连接的另一个点。通过这种技巧,就可以排除掉同一条边而保留重边。
/**
* Author : correct
*/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cstdlib>
#define mem(a ,b) memset(a, b, sizeof a)
#define ll long long
using namespace std;
const int N = 100100, M = 1000100;
int head[N], nex[M], to[M], cnt;
int dfn[N], low[N], num;
bool bridge[M];
int root;
int n, m;
void add(int a, int b){
to[++cnt] = b;
nex[cnt] = head[a];
head[a] = cnt;
}
void tarjan(int x, int ind){
dfn[x] = low[x] = ++num;
for (int i = head[x]; i; i = nex[i]){
int y = to[i];
if (!dfn[y]){
tarjan(y, i);
low[x] = min(low[x], low[y]);
if (dfn[x] < low[y]){
bridge[i] = bridge[i ^ 1] = 1;
}
}
else if (i != (ind ^ 1)){
low[x] = min(low[x], dfn[y]);
}
}
}
int main()
{
freopen("in.in", "r", stdin);
cnt = 1;
ios::sync_with_stdio(0);
cin >> n >> m;
while (m--){
int a, b;
cin >> a >> b;
add(a, b), add(b, a);
}
for (int i = 1; i <= n; i++){
if (!dfn[i]){
tarjan(i, 0);
}
}
for (int i = 2; i <= cnt ;i += 2){
if (bridge[i]){
cout << to[i] << " -> " << to[i ^ 1] << "\n";
}
}
return 0;
}