基本概念
给定无向连通图G = (V, E)
割点:
对于x∈V,从图中删去节点x以及所有与x关联的边之后,G分裂为两个或两个以上不相连的子图,则称x为割点
割边(桥)
若对于e∈E,从图中删去边e之后,G分裂成两个不相连的子图,则称e为G的桥或割边
时间戳
在图的深度优先遍历过程中,按照每个节点第一次被访问的时间顺序,依次给予N个节点1~N的整数标记,该标记被称为“时间戳”,记为dfn[x]
搜索树
在无向连通图中任选一个节点出发进行深度优先遍历吗,每个节点只访问一次。所有发生递归的边(x, y)构成一棵树,称为“无向连通图的搜索森林”。一般无向图的各个连通块的搜索树构成无向图的“搜索森林”。对于深度优先遍历出的搜索树,按照被遍历的次序,标记节点的时间戳
追溯值
追溯值low[x]。设subtree(x)表示搜索树中以x为根的子树。low[x]定义为以下节点时间戳的最小值
low[u]定义为u或者u的子树中能够通过非父子边(父子边就是搜索树上的边)追溯到的最早的节点的时间戳
即:
1.subtree(x)中的节点
2.通过一条不在搜索树上的边,能够到达subtree(x)的节点
为了计算low[x],应该先令low[x] = dfn[x],然后考虑从x出发的每条边(x, y):
若在搜索树上x是y的父节点,则令low[x] = min(low[x], low[y])
若无向边(x, y)不是搜索树上的边,则令low[x] = min(low[x], dfn[y])
桥的判定法则
无向边(x, y)是桥,当且仅当搜索树上存在x的一个子节点y,满足:
dfn[x] < low[y]
根据定义,dfn[x] <low[y]说明从subtree(y)出发,在不经过(x, y)的前提下,不管走哪条边,都无法到达x或比x更早访问的节点。若把(x, y)删除,则subtree(y)就好像形成了封闭的环境,与节点x没有边相连,图断成了两部分,(x, y)为桥
反之,若不存在这样的子节点x和y,使得dfn[x] < low[y],这说明每个subtree(y)都能绕行其他边到x或比x更早的节点,(x, y)也就不是桥
桥一定是搜索树中的边,并且一个简单环中的边一定都不是桥
需要注意的是, 因为我们要遍历的是无向图, 所以从每个节点x出发,总能访问到他的父节点fa,根据low的计算方法,(x, fa)属于搜索树上的边,且fa不是x的子节点,故不能用fa的时间戳来更新low[x]。
如果仅记录每个节点的父节点,会无法处理重边的情况——当x与fa之间有多条边时,(x, fa)一定不是桥,在这些重复计算中,只有一条边在搜索树上,其他的几条都不算,故有重边时,dfn[fa]不能用来更新low[x]
解决方案是:记录“递归进入每个节点的边的编号”。编号可认为是边在邻接表中储存下标位置。把无向图的每条边看做双向边,成对存储在下标"2和3","4和5","6和7"...处。若沿着编号i的边递归进入节点x,则忽略从x出发的编号为i xor 1的边,通过其他边计算low[x]即可
(补充:^的成对变换
对于非负整数n
当n为偶数时,n xor 1等于n+1
当n为奇数时,n xor 1等于n-1
因此“0与1”“2与3”“3与5”……关于xor 1运算构成了成对变换
这一性质经常用于图论邻接表中边集的存储。在具有无向边(双向边)的图中把一对正反方向的边分别储存在邻接表数组的第n与n+1个位置(其中n为偶数),就可以通过xor 1运算获得与当前边(x, y)反向的边(y, x)的存储位置
在程序开始时,初始化变量tot = 1。这样每条无向边看成的两条有向边会成对存储在ver和edge数组的下表“2和3”“4和5”“6和7”……的位置上。通过对下表xor 1操作,就可以直接定位到与当前反向的边。换句话说,如果ver[i]是第i条边的终点,那么ver[i ^ 1]就是第i条边的起点)
1 #include<bits/stdc++.h> 2 using namespace std; 3 const int maxn = 100086; 4 struct node { 5 int y, net; 6 }e[maxn << 1]; 7 int lin[maxn], len = 1; 8 bool bridge[maxn << 1]; 9 int dfn[maxn], low[maxn]; 10 int n, m, num; 11 12 inline int read() { 13 int x = 0, y = 1; 14 char ch = getchar(); 15 while(!isdigit(ch)) { 16 if(ch == '-') y = -1; 17 ch = getchar(); 18 } 19 while(isdigit(ch)) { 20 x = (x << 1) + (x << 3) + ch - '0'; 21 ch = getchar(); 22 } 23 return x * y; 24 } 25 26 inline void insert(int xx, int yy) { 27 e[++len].net = lin[xx]; 28 e[len].y = yy; 29 lin[xx] = len; 30 } 31 32 inline void tarjan(int x, int in_edge) { 33 dfn[x] = low[x] = ++num; 34 for(int i = lin[x]; i; i = e[i].net) { 35 int to = e[i].y; 36 if(!dfn[to]) { 37 tarjan(to, i); 38 low[x] = min(low[x], low[to]); 39 if(low[to] > dfn[x]) 40 bridge[i] = bridge[i ^ 1] = true; 41 } 42 else if(i != (in_edge ^ 1)) 43 low[x] = min(low[x], dfn[to]); 44 } 45 } 46 47 int main() { 48 n = read(), m = read(); 49 len = 1; 50 for(int i = 1; i <= m; ++i) { 51 int x, y, z; 52 x = read(), y = read(); 53 insert(x, y); 54 insert(y, x); 55 } 56 for(int i = 1; i <= n; ++i) 57 if(!dfn[i]) tarjan(i, 0); 58 for(int i = 2; i < len; i += 2) 59 if(bridge[i]) 60 cout << e[i ^ 1].y << ' ' << e[i].y << '\n'; 61 return 0; 62 }