在上一节我们已经知道tarjan算法可以求联通图,在这里我们也运用tarjan的思想求割点与割边,首先我们先来说说割点,那么什么事割点呢,先来看一张图(a),图片来自网络
在(a)图中,我们将A点以及与A点相连的边全部去除,会发现这个联通图被分成了俩个联通图,一个是节点F,另外一个是余下的所有的节点组成的图,因此我们将A点称为割点,同理我们发现B点也是割点,因此我们可以这样定义割点,在一个无向联通图中,如果删除某个节点后,图不再连通(即任意俩点之间不能相互到达),我们称这样的顶点为割点(定义来自啊哈算法),那么问题来了,如何求割点呢???
很容易的一个想法就是我们每次删除一个节点后用bfs或者dfs来遍历图是否依然联通,如果不联通则该点是割点,这种方法的复杂度是O(N(N + M)),太暴力了,我们来想想其他的方法
再介绍其他方法前,我们再来了解几个定义(定义来源于网络)
- DFS搜索树:用DFS对图进行遍历时,按照遍历次序的不同,我们可以得到一棵DFS搜索树,如图(b)所示。
- 树边:(在[2]中称为父子边),在搜索树中的实线所示,可理解为在DFS过程中访问未访问节点时所经过的边。
- 回边:(在[2]中称为返祖边、后向边),在搜索树中的虚线所示,可理解为在DFS过程中遇到已访问节点时所经过的边。
通过观察图(2),我们发现有俩类节点可能成为割点
- 对根节点u,若其有两棵或两棵以上的子树,则该根结点u为割点;(说明删除该节点会产生至少俩棵不联通的子树)
- 对非叶子节点u(非根节点),若其子树的节点均没有指向u的祖先节点的回边,说明删除u之后,根结点与u的子树的节点不再连通;则节点u为割点。(在这里可以简单画个图理解一下)
对于这俩类节点,前者我们很容易判断,我们就不做过多的说明了,具体的判断会在代码里面讲解,我们来说说后者的判断
我们用dfn[u]
记录节点u在DFS过程中被遍历到的次序号,low[u]
记录节点u或u的子树通过非父子边追溯到最早的祖先节点(即DFS次序号最小),那么low[u]的计算过程如下:
low[u]={min{low[u], low[v]},((u,v)为树边)
low【u】 = min{low[u], dfn[v]} (u,v)为回边且v不为u的父亲节点
至于为什么不为u的父亲节点我暂时还没有想明白......
关于dfn和low数组的计算请参考我另外一篇博客或者去百度上查询资料,这个不好用语言描述,我就暂且大家都知道了
那么对于第二类节点,当(u,v)为树边且low[v] >= dfn[u]
时,节点u才为割点。该式子的含义:表示v节点不能绕过u节点从而返回到更早的祖先,因此u节点为割点
下面来看代码实现,在代码里面会做详细的解说
#include<iostream>
#include<cmath>
using namespace std;
int n, m, root, a, b, total;
int e[101][101], dfn[101], low[101], flag[101], head[101];
//链式前向星,不懂的自行百度
struct node{
int to;
int next;
}edge[10010];
int cnt = 1;
//前向星建图
void add(int u, int v) {
edge[cnt].to = v;
edge[cnt].next = head[u];
head[u] = cnt++;
}
void tarjan(int u, int father) {
int child = 0;//child用来记录在生成树中当前顶点的儿子的个数
dfn[u] = low[u] = ++total;//时间戳
for (int i = head[u]; i != 0; i = edge[i].next) {//前向星遍历该顶点的所有边
int v = edge[i].to;//该顶点连接的顶点
if (!dfn[v]) {//如果时间戳为0说明没有访问过
child++;
tarjan(v, u);//继续往下搜索
low[u] = min(low[u], low[v]);//更新当前时间戳
// 如果当前节点是根节点并且儿子个数大于等于2,则满足第一类节点,为割点
if (u == root && child >= 2) {
flag[u] = 1;
//不为根结点但是满足第二类条件的节点
} else if (u != root && low[v] >= dfn[u]) {
flag[u] = 1;
}
//如果顶点被访问过并且不是该节点的父亲,说明此时的v为u的祖先,因此需要更新最早顶点的时间戳
} else if (v != father) {
low[u] = min(low[u], dfn[v]);
}
}
}
int main() {
cin >> n >> m;
for (int i = 0; i < m; i++) {
cin >> a >> b;
add(a, b);
add(b, a);
}
root = 1; //假设1为根节点
//从1号顶点开始进行深度优先搜索(tarjan)
tarjan(1, root);
for (int i = 1; i <= n; i++) {
if(flag[i]) {
cout << i << " ";
}
}
return 0;
}
给一组数据拿来测试一下该代码
6 7(6个顶点,7条无向边)
1 4
1 3
4 2
3 2
2 5
2 6
5 6
输出为 2
如果有多组图,那么我们使用另外的一组代码,见下
#include<iostream>
using namespace std;
const int maxn = 20010;
const int maxv = 200010;
int cnt = 1, n, m, total, a, b;
int head[maxn], flag[maxn], dfn[maxn], low[maxn], sum;
struct node{
int to;
int next;
}edge[maxv];
void add(int u, int v) {
edge[cnt].to = v;
edge[cnt].next = head[u];
head[u] = cnt++;
}
void tarjan(int u, int father) {
int child = 0;
dfn[u] = low[u] = ++total;
for (int i = head[u]; i != 0; i = edge[i].next) {
int v = edge[i].to;
if (!dfn[v]) {
tarjan(v, father);
low[u] = min(low[u], low[v]);
if (low[v] >= dfn[u] && u != father) {
flag[u] = 1;
}
if (u == father) child++;
}
low[u] = min(low[u], dfn[v]);
}
if (child >= 2 && u == father) {
flag[u] = 1;
}
}
int main () {
cin >> n >> m;
for (int i = 0; i < m; i++) {
cin >> a >> b;
add(a, b);
add(b, a);
}
for (int i = 1; i <= n; i++) {
if (!dfn[i]) {
tarjan(i, i);
}
}
for (int i = 1; i <= n; i++) {
if (flag[i]) sum++;
}
cout << sum << endl;
for (int i = 1; i <= n; i++) {
if (flag[i]) {
cout << i << " ";
}
}
return 0;
}
了解完割点后我们再来了解什么是割边,其实很简单,即在一个无向联通图中,如果删除某条边后,图不在联通,那么该条边称为割边,代码和上面的代码基本上一模一样,只要改一个小地方,就是将low【v】 >= dfn[u] 改为 low【v】> dfs
[u]即可,前者便是还可以回到父亲,后者表示连父亲都回不到了,倘若顶点v不能回到祖先,也没有另外一条路可以回到父亲,那么u-v这条边就是割边,其实仔细想想模拟个图就明白了,代码实现如下
#include<iostream>
#include<cmath>
using namespace std;
int n, m, root, a, b, total;
int e[101][101], dfn[101], low[101], flag[101], head[101];
struct node{
int to;
int next;
}edge[10010];
int cnt = 1;
void add(int u, int v) {
edge[cnt].to = v;
edge[cnt].next = head[u];
head[u] = cnt++;
}
void tarjan(int u, int father) {
int child = 0;
dfn[u] = low[u] = ++total;
for (int i = head[u]; i != 0; i = edge[i].next) {
int v = edge[i].to;
if (!dfn[v]) {
child++;
tarjan(v, u);
low[u] = min(low[u], low[v]);
if (low[v] > dfn[u]) {
cout << u << "->" << v << endl;
}
} else if (v != father) {
low[u] = min(low[u], dfn[v]);
}
}
}
int main() {
cin >> n >> m;
for (int i = 0; i < m; i++) {
cin >> a >> b;
add(a, b);
add(b, a);
}
root = 1;
tarjan(1, root);
for (int i = 1; i <= n; i++) {
if(flag[i]) {
cout << i << " ";
}
}
return 0;
}
如果输入上面的那组数据那么不会有输出,因为没有割边,再给出一组数据
6 6
1 4
1 3
4 2
3 2
2 5
5 6
输出5->6
2->5
割点和割边就解释到这里了,完毕