割点的概念
在无向连通图中,如果将其中一个点以及所有连接该点的边去掉,图就不再连通,那么这个点就叫做割点(cut vertex / articulation point)。
例如,在下图中,0、3是割点,因为将0和3中任意一个去掉之后,图就不再连通。如果去掉0,则图被分成1、2和3、4两个连通分量;如果去掉3,则图被分成0、1、2和4两个连通分量。
怎么求割点
可以使用Tarjan算法求割点(注意,还有一个求连通分量的算法也叫Tarjan算法,与此算法类似,参见here)。(Tarjan,全名Robert Tarjan,美国计算机科学家。)
首先选定一个根节点,从该根节点开始遍历整个图(使用DFS
)。
对于根节点,判断是不是割点很简单——计算其子树数量,如果有2棵即以上的子树,就是割点。因为如果去掉这个点,这两棵子树就不能互相到达。
对于非根节点,判断是不是割点就有些麻烦了。我们维护两个数组dfn[]
和low[]
,dfn[u]
表示顶点u第几个被(首次)访问,low[u]
表示顶点u及其子树中的点,通过非父子边(回边),能够回溯到的最早的点(dfn
最小)的dfn
值(但不能通过连接u与其父节点的边)。对于边(u, v)
,如果low[v]>=dfn[u]
,此时u
就是割点。
但这里也出现一个问题:怎么计算low[u]
。
假设当前顶点为u,则默认low[u]=dfn[u]
,即最早只能回溯到自身。
有一条边(u, v)
,如果v
未访问过,继续DFS
,DFS
完之后,low[u]=min(low[u], low[v])
;
如果v
访问过(且u
不是v
的父亲),就不需要继续DFS
了,一定有dfn[v]<dfn[u],low[u]=min(low[u], dfn[v])
。
参考博文:https://www.cnblogs.com/collectionne/p/6847240.html
代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=20000;
const int MAXM=100000+10;
int dfn[MAXN],low[MAXN];
bool cut[MAXN];
int ans;
int n,m,deep;
struct Node
{
int to,next;
}edge[MAXM*2];
int cnt,head[MAXN];
inline int read()
{
int tot=0;
char c=getchar();
while(c<'0'||c>'9')
c=getchar();
while(c>='0'&&c<='9')
{
tot=(tot<<1)+(tot<<3)+c-'0';
c=getchar();
}
return tot;
}
inline void add(int x,int y)
{
edge[++cnt].to=y;
edge[cnt].next=head[x];
head[x]=cnt;
}
inline void tarjan(int u,int father)
{
int tot=0;
low[u]=dfn[u]=++deep;
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].to;
if(!dfn[v])
{
tarjan(v,u);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u]&&u!=father)cut[u]=1;
if(u==father)tot++;
}
low[u]=min(low[u],dfn[v]);
}
if(u==father&&tot>=2)cut[u]=1;
}
int main()
{
n=read();m=read();
for(int i=1;i<=m;i++)
{
int x=read(),y=read();
add(x,y);add(y,x);
}
/*for(int i=1;i<=n;i++)
for(int j=head[i];j;j=edge[j].next)
cout<<i<<" "<<edge[j].to<<endl;*/
for(int i=1;i<=n;i++)
if(!dfn[i])tarjan(i,i);
for(int i=1;i<=n;i++)
ans+=cut[i];
printf("%d
",ans);
for(int i=1;i<=n;i++)
if(cut[i])printf("%d ",i);
printf("
");
return 0;
}