割点和割边
给出一个无向连通图, 求出所有割点与割边的数量。
输入
第1行: 2个整数N,M (1 <= N <= 5,000,N-1 <= M <= 10,000),分别表示顶点数和边数
接下来M行,每行2个整数,表示图中的一条边。
输出
第1行:1个整数,表示割点数
第2行:1个整数,表示割边数
样例输入
11 13
1 2
1 4
1 5
1 6
2 11
2 3
4 3
4 9
5 8
5 7
6 7
7 10
11 3
样例输出
4
3
概念
割点的定义(均在无向图中):在一个连通图中,如果有一个顶点集合,删除这个集合中的点以及相关的边之后,连通块的数量会增多.我们就称这个顶点集合为割点集合.如果这个点集合中只有一个点,那么这个点就叫做割点.
割边的定义(均在无向图中):在一个连通图中,如果删去其中一条边后,连通块的数量会增多,那么我们称这条边为桥或者是割边.
Tarjan算法
首先选择一个根节点,从它开始进行DFS遍历.
对于根节点来说,判断它是否为割点是十分容易的.我们只需要看它的字树的数量即可.若它的子树的数量>=2,那么它必定为一个割点.(使得子树与子树之间失去了联系)
那么我们只需要求非根节点是否为割点即可.
首先,我们需要定义两个数组:low[]和dfn[].
dfn[u]为u点在上述dfs中是被访问到的第几个点。
low[u]为从u点及u点的子孙出发仅经过一次回边能到达的最小dfn。(如上图low[2]=1 2>3>1)
割点
如果一条边满足low[v]>=dfn[u],那么u点即为一个割点.
证明:如果存在这样一条边满足这样的性质,那么u的儿子v就永远不会访问到早于u的点,那么也就是说,从v出发形成的环中不会包括u,那么从u断开的话,就会形成两个或多个连通块,满足了割点的需求.
首先,low[u]可以先初始化为dfn[u],我们认为u至少能通过回边访问到自己(其实也就是没有回边).然后我们在遍历的过程中,考虑两种情况:一种是v点还没有被访问过,那么它的low[u]=min(low[u],low[v])(此时low[v]已经在dfs的过程中求出来了(在回溯的过程中),v能访问到的最早的点u同样也能访问到).
在过程中如果访问到一个已经访问过的点,那么有low[u]=min(low[u],dfn[v]).然后返回.
割边
割边的求法类似于割点.之前我们说到,在判断非根节点是否为割点时,我们采用了看low[v]>=dfn[u]的做法,在这里我们的做法更加简单.
不需要考虑根节点的问题,只需要判定low[v]>dfn[u]即可.(如之前给出来的图)
Code
#include<cstdio>
#include<vector>
using namespace std;
#define MAXN 5001
vector<int>G[MAXN];
int Dfn[MAXN],Low[MAXN];
bool Vis[MAXN];
int n,m;
int cp=0,ce=0,dcnt=0,rtson=0;//计数器
//cp点数 ce边数
//dcnt当前点的发现时间(第一次遍历到的序号)
//rtson
inline int Min(int x,int y){//取两数较小
return x<y?x:y;
}
void Dfs(int u,int fa){//当前节点与父亲节点
Low[u]=Dfn[u]=++dcnt;//Low值初值与Dfn一样
for(int i=0;i<G[u].size();i++){ //枚举相连点
int v=G[u][i];
if(!Dfn[v]){//没有遍历过
Dfs(v,u);//搜索
/*回溯处理*/
Low[u]=Min(Low[u],Low[v]);//更新Low值
if(Low[v]>=Dfn[u]){ //判断割点
if(fa!=-1){
if(!Vis[u]){ //判重
cp++;
Vis[u]=true;
}
}
else rtson++;
}
if(Low[v]>Dfn[u])ce++; //判断割边
}
else if(v!=fa)
Low[u]=Min(Low[u],Dfn[v]);
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=0;i<m;i++){
int u,v;
scanf("%d%d",&u,&v);
G[u].push_back(v); //邻接矩阵
G[v].push_back(u);
}
Dfs(1,-1);
if(rtson>1)cp++;
printf("%d\n%d\n",cp,ce);
}