图的割点和割边
首先得弄清楚割点的概念,割点是什么?在一个无向连通图中,如果删除某个顶点后,图不在连通(即任意两点之间不能互相到达),我们称这样的点为割点,而割点如何求呢?
假如我们在深度优先遍历时访问到了k点,这时图就被分割成两部分。一部分是被访问过的点,另一部分则是没有被访问过的点。如何k是割点,那么剩下的没有被访问过的点至少有一个在不经过k点的情况下,是无论如何都再也回不到已访问的点了。那么这个连通图就被分割成多个不连通的子图了。
连通图如下所示:
还要弄清楚一个概念,时间戳。表示这个顶点在遍历时第几个被访问到的。我们用数组num来记录每个顶点的时间戳。用low数组来记录每个顶点能回到的最小时间戳。
这个算法的关键是在于:当深度优先遍历到顶点u时,假设图中还有顶点v是没有被访问到的点,如何判断顶点v在不经过u的情况下还能回到之前从访问过的点。如果从生成树的角度来说,顶点u就是顶点v的父节点,顶点v是顶点u的儿子,而之前访问过的顶点就是祖先。这里是对顶点v再进行一次深度优先遍历,但是此次不允许通过顶点u,看看是否能回到祖先。不能回到则是割点。
核心代码:
//割点核心算法
void dfs(int cur,int father)
{
index ++; //时间戳加1
num[cur] = index;
low[cur] = index;
for(int i = 1; i <= n; i ++)
{
if(e[cur][i] == 1) //顶点未被访问过
{
if(num[i] == 0)
{
child ++;
dfs(i,cur); //继续往下深度优先遍历
low[cur] = min(low[cur],low[i]); //更新当前顶点能访问到的最小时间戳
//当前顶点cur不是根节点并且满足low[i]>=num[cur],当前顶点为割点
if(cur != 1 && low[i] >= num[cur])
{
flag[cur] = 1;
}
//当前顶点为割点,并且只有两个儿子
if(cur == 1 && child == 2)
{
flag[cur] = 1;
}
}else
//否则曾经被访问过,更新cur能访问到的最小时间戳
if(i != father)
{
low[cur] = min(low[cur],num[i]);
}
}
}
}
全部代码:
#include<iostream>
#define maxPath 99999999
using namespace std;
int n,m;
int e[1001][1001];
int num[1001]; //记录顶点访问时间戳
int low[1001]; //记录顶点能回到的最小时间戳
int index,child; //记录时间
int flag[1001];
int min(int a,int b)
{
return a > b? b : a;
}
//割点核心算法
void dfs(int cur,int father)
{
index ++; //时间戳加1
num[cur] = index;
low[cur] = index;
for(int i = 1; i <= n; i ++)
{
if(e[cur][i] == 1) //顶点未被访问过
{
if(num[i] == 0)
{
child ++;
dfs(i,cur); //继续往下深度优先遍历
low[cur] = min(low[cur],low[i]); //更新当前顶点能访问到的最小时间戳
//当前顶点cur不是根节点并且满足low[i]>=num[cur],当前顶点为割点
if(cur != 1 && low[i] >= num[cur])
{
flag[cur] = 1;
}
//当前顶点为割点,并且只有两个儿子
if(cur == 1 && child == 2)
{
flag[cur] = 1;
}
}else
//否则曾经被访问过,更新cur能访问到的最小时间戳
if(i != father)
{
low[cur] = min(low[cur],num[i]);
}
}
}
}
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= n; j ++)
if(i == j)
e[i][j] = 0;
else
e[i][j] = maxPath;
for(int i = 1; i <= m; i ++)
{
int x,y;
scanf("%d%d",&x,&y);
e[x][y] = 1;
e[y][x] = 1;
}
dfs(1,1);
for(int i = 1; i <= n; i ++)
{
if(flag[i] == 1)
cout << i << " ";
}
return 0;
}
割边与割点差不多,只需将low[v] >= num[u]代表的是不可能在不经过父亲节点u而回到祖先的,而low[v] > num[u]则是连父亲都回不到了。
核心代码:
//割边核心算法
void dfs(int cur,int father)
{
index ++; //时间戳加1
num[cur] = index;
low[cur] = index;
for(int i = 1; i <= n; i ++)
{
if(e[cur][i] == 1) //顶点未被访问过
{
if(num[i] == 0)
{
dfs(i,cur); //继续往下深度优先遍历
low[cur] = min(low[cur],low[i]); //更新当前顶点能访问到的最小时间戳
//当前顶点cur不是根节点并且满足low[i]>=num[cur],当前顶点为割点
if(low[i] > num[cur])
{
printf("%d-%d\n",cur,i);
}
}else
//否则曾经被访问过,更新cur能访问到的最小时间戳
if(i != father)
{
low[cur] = min(low[cur],num[i]);
}
}
}
}
补充一下,割点和割边算法是由Robert E.Tarjan 发明的。