在一个无向连通图中,如果删除某一顶点后,图再连通(即任意两点之间不能相互到达),我们称这样的顶点为割点(或者割顶)。
首先我们从图中任意一个点开始对图进行遍历,而对图进行深度优先遍历将会得到这个图的一个生成树(这个生成树并不一定是最小生成树),我们用数组 num 记录某个顶点是第几个被访问过的即 “时间戳”。
如何在遍历的时候认定一个顶点是割点,假如我们在深度优先遍历时访问到了 k 点,此时就会被 k 点分割成为两个部分。一部分是访问过的点一部分是未被访问过的点。 如果 k 点是割点,那么剩下的没有被访问过的点中至少会有一个点在不经过 k 点的情况下,是无论如何再也回不到已访问过的点,那么一个连通图就会被 k 点分割成多个不连用的子图。
到的这里我们需要解决的是,顶点 v 在不经过父顶点 u 的情况下还能回到祖先,我们的方法是对顶点v 在进行一次深度优先遍历,但是此次遍历时不允许 经过父节点 u ,看是否能回到祖先,如果不能那么这个点 u 就是割点。
前面我们遍历时已经记录生成树中每个数的时间戳,为了不重复计算,我们需要一个数组 low 来记录每个顶点在不经过父节点时能够回到的最小的时间戳。
对于一个顶点 u ,如果存过至少一个顶点 v (即顶点 u 的儿子)是的 low [v]>=num[u],即不能回到祖先,那么 u 为割点。
具体代码模板如下:
#include<iostream>
#include<algorithm>
using namespace std;
int n,m,e[10][10],root;
int num[10],low[10],flog[10],inder;
int min(int a,int b)
{
return a<b?a:b;
}
void dfs(int now1,int father)
{
int child=0,i; // child 用来记录在生成树中当前顶点 now1 的儿子个数
inder++;
num[now1]=inder; // 当前顶点的时间戳
low[now1]=inder; // 当前顶点 now1 能够访问到的最早顶点的时间戳,刚开始就是自己
for(i=1;i<=n;i++) // 枚举与当前顶点now1 有边相连的顶点 i
{
if(e[now1][i]==1)
{
if(num[i]==0) // 如果顶点 i 的时间戳为 0 ,说明顶点i还没有被访问过
{
child++; // 从生成树的角度来说,此时 i 为 now1 的儿子
dfs(i,now1); // 继续往下深度优先遍历
// 更新当前顶点 now1 能访问的最早的时间戳
low[now1]=min(low[now1],low[i]);
// 如果当前顶点不是根节点并且满足 low[i] >= num[now1],则当前顶点为割点
if(now1!=root&&low[i]>=num[now1])
{
flog[now1]=1;
}
// 如果当前顶点是根节点,在生成树中根节点必须要有两个儿子,那么这个根节点才能是割点
if(now1==root&&child==2)
{
flog[now1]=1;
}
}
// 否则如果顶点 i 曾经被访问过,并且这个顶点不是当前顶点 now1 的父亲
// 则说明此时的 i 为now1 的祖先,因此需要更新当前节点 now1 能访问到最早顶点的时间戳
else if(i!=father)
{
low[now1]=min(low[now1],num[i]);
}
}
}
return;
}
int main()
{
int i,j,x,y;
cin>>n>>m;
for(i=1;i<=n;i++)
{
for(j=1;j<=n;j++)
{
e[i][j]=0;
}
}
for(i=1;i<=m;i++)
{
cin>>x>>y;
e[x][y]=1;
e[y][x]=1;
}
root=1;
dfs(1,root); //从 1 号顶点开始进行深度优先遍历
for(i=1;i<=n;i++)
{
if(flog[i]==1)
{
cout<<i<<"\n";
}
}
return 0;
}