强连通割点
定义
割点定义:如果去掉了 $i$ 号点和关于 $i$ 的所有的边,包含i的那个连通块(也就是联通分量)如果被分成了两个连通块,那么 $i$ 就是割点。
思路
-
输入
-
$dfs$ 没有遍历过的点
我们在 $dfs$时记录两个变量:表示遍历序和遍历序最小的祖先的遍历序。我们在搜索时可以直接判断,要是一个点连向的另外一个点和已遍历过的所有点如果都不联通,那就证明当前搜索到的这个点是割点。
-
计数和输出
对于割点数目计数,逐一输出割点编号
代码
#include<bits/stdc++.h>
using namespace std;
const int N=200010;
int a[N],nxt[N],head[N];
int dfn[N],low[N],cnt,k;//low数组表示能会到的最早遍历到的祖先点,也就是祖先里最小的dfn值;cnt用来记录遍历顺序;k为建边数组的长度
bool cut[N],bst[N];//cut数组来记录第i个点是不是割点
void add(int x,int y){//用邻接表(可以叫链表或链式前向星)来存储边,因为数据范围比较大
a[++k]=y;//连向的点
nxt[k]=head[x];//记录下一条边(起点为x的边)
head[x]=k;//记录同一个起点最早出现的边的编号
}
void tarjan(int u,int mr){
int rc=0;
dfn[u]=low[u]=++cnt;//记录初始的遍历顺序和遍历序号最小的祖先
for(int i=head[u];i;i=nxt[i]){
int v=a[i];//这条边连向的终点
if(!dfn[v]){//如果没有遍历过,因为是深搜,如果不加这个条件它就会一直搜下去
tarjan(v,mr);//递归,继续对与v点进行深搜
low[u]=min(low[u],low[v]);//搜完时(及更新完v点能到达的最早遍历的祖先时)再更新最早能到达的祖先(毕竟u,v是联通的,v能到达的u一定能到达)
if(low[v]>=dfn[u]&&u!=mr){//如果v最早能到达的点的编号大于等于u的遍历编号,证明它到不了之前搜过的点,证明v如果不经过u的话和之前到达的点是不联通的,也就是相当于只要没了u,就会被割成两部分
cut[u]=1;//标记u是割点
}
if(u==mr){//如果这个点是通过起点扩展出去的
rc++;//起点扩展出去的点数加1
}
}
low[u]=min(low[u],dfn[v]);//取能到达的点的遍历序号的最小值
}
if(rc>=2){//如果起点扩展出去的点数大于等于2,也就证明先从起点搜出了能到达的第1部分,但是还有点是从起点连出的并且没有和第1部分相连(相连的话是能直接搜到标记掉的,但这里没标记)
cut[mr]=1;//标记这个起点为割点
}
}
int main(){
int n,m,ans=0;
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int x,y;
scanf("%d%d",&x,&y);
add(x,y);//无向图的双向边之一
add(y,x);//无向图的双向边之一
}
for(int i=1;i<=n;i++){
if(!dfn[i]){//如果没搜过
tarjan(i,i);//搜一遍
}
}
for(int i=1;i<=n;i++){
if(cut[i]){
ans++;//对于割点数目计数
}
}
printf("%d\n",ans);
for(int i=1;i<=n;i++){
if(cut[i]){//如果是割点
printf("%d ",i);//输出这个点的编号
}
}
return 0;
}