广场舞老奶奶都看懂的割点、割边、边双连通分量和点双连通分量详解

这一期我们来讲解图论里面的割点、割边、边双连通分量和点双联通分量。

要解决这些问题,我们要再次请出我们的Tarjan大佬。没错,这几个问题的算法也是他发明的。

所以,我们要用dfs来解决这个问题。

我们先来解决割点。

割点:

割点的概念:在无向连通图中,如果将其中一个点以及所有连接该点的边去掉,图中连通分量数量变多,那么这个点就叫做割点

如何求呢?

最容易想到的就是一次删除每个节点,然后dfs判断是否连通。

这个方法效率太低了,而且太low了,我们这里用一下更高级的方法。

这里我们需要用到dfs序,我们定义dfn[i]为dfs到i点时的序号。这里我们还需要定义一个东西——low[i],它代表什么呢?它表示low[u]表示顶点u及其子树中的点,通过非父子边,能够回溯到的最早的点(dfn最小)的dfn值(但不能通过连接u与其父节点的边)。

如何求解呢?我们可以在dfs中求解。

假设当前顶点为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])。

对于节点u,如果它有一个子节点v,使得low[v]>=dfn[u],那么此时u就是割点。

对于根节点,如果此时它有两个子节点,都使得low[root]>=dfn[u],那么此时根节点就是割点

这样子我们就可轻松求解了。

CODE:

#include<bits/stdc++.h>
using namespace std;
int n,m;
vector<int> a[100005];
int dfn[100005],low[100005];
int t=1;
int ans=0;
bool flag[100005];
void dfs(int x,int fa){
	dfn[x]=t++;
	low[x]=dfn[x];
	int child=0;
	for(int ti=0;ti<a[x].size();ti++){
		int i=a[x][ti];
		if(dfn[i]==0){
			child++;
			dfs(i,x);
			if(dfn[x]<=low[i] && x!=1)
				flag[x]=1;
			else if(x==1 && child==2)
				flag[x]=1;
			if(low[i]<low[x])
				low[x]=low[i];
		}
		else if(i!=fa&&dfn[i]<low[x])
				low[x]=dfn[i];
	}
}
int main(){
    cin>>n>>m;
    int tx,ty;
    for(int i=1;i<=m;i++){
    	cin>>tx>>ty;
    	a[tx].push_back(ty);
    	a[ty].push_back(tx);
	}
	dfs(1,0);
	for(int i=1;i<=n;i++)
		if(flag[i])
			ans++;
	cout<<ans;
}

模板题:P3388 【模板】割点(割顶) 

割边:

割边的概念:在无向连通图中,如果将其中一条边去掉,图中连通分量数量变多,那么这个点就叫做割点

跟割点类似,我们定义的low和dfn数组的意义不变,但是判断(u,v)的标准变成当且仅当(u,v)为搜索树中的边并且dfn(u)<low(v) 。

所以我们也可以轻松解决。

CODE:

#include<bits/stdc++.h>
using namespace std;
vector<int> a[100005];
int dfn[100005],low[100005],t=1,ans=0,n,m;
bool flag[100005];
void dfs(int x,int fa){
	dfn[x]=t++;
	low[x]=dfn[x];
	int child=0;
	for(int ti=0;ti<a[x].size();ti++){
		int i=a[x][ti];
		if(dfn[i]==0){
			child++;
			dfs(i,x);
			if(dfn[x]<low[i])ans++;
			if(low[i]<low[x])low[x]=low[i];
		}
		else if(i!=fa&&dfn[i]<low[x])low[x]=dfn[i];
	}
}
int main(){
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        int tx,ty;
    	cin>>tx>>ty;
    	a[tx].push_back(ty);
    	a[ty].push_back(tx);
	}
	dfs(1,0);
	cout<<ans;
}

模板题:【模板】割边 (注:输入后是连一条无向边,题目描述有误)

好,简单的割点和割边相信大家都会了,我们接下来来讲解边双连通分量和点双连通分量。

边双连通分量: 

边双连通分量的概念: 对于一个无向图中的极大边双连通(没有割边)的子图,我们称这个子图为边双连通分量

如果我们画一个图,再把割边标记出来,我们不难看出每个边双连通分量都被割边所分开,所以,求边双连通分量我们可以先求割边,再标记出来,在不走割边的情况下跑dfs,最后输出。

CODE:

#include <bits/stdc++.h>
using namespace std;
const int maxn=5e5+10,maxm=4e6+10;
int b[maxm],n,m,cnt=1,ans,id,dfn[maxn],low[maxn],head[maxn],dcc[maxn];
struct edge{
	int to,nxt;
}e[maxm];
vector<vector<int> >Ans;
void add(int f,int t){
	e[++cnt].to=t;
	e[cnt].nxt=head[f];
	head[f]=cnt;
}
void tarjan(int node,int in_edge){
	dfn[node]=low[node]=++id;
	for (int i=head[node];i;i=e[i].nxt){
		const int to=e[i].to;
		if(dfn[to]==0){
			tarjan(to,i);
			if (dfn[node]<low[to])b[i]=b[i^1]=1;
			low[node]=min(low[node],low[to]);
		}
		else if(i!=(in_edge^1))
			low[node]=min(low[node],dfn[to]);
	}
}
void dfs(int node,int ndcc){
	dcc[node]=ndcc;
	Ans[ndcc-1].push_back(node);
	for (int i=head[node];i;i=e[i].nxt){
		int to=e[i].to;
		if(dcc[to]||b[i])continue;
		dfs(to,ndcc);
	}
}
int main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for (int i=1;i<=m;i++){
		int f,t;
		cin>>f>>t;
		if(f!=t)add(f,t),add(t,f);
	}
	for(int i=1;i<=n;i++)if(dfn[i]==0)tarjan(i,0);
	for(int i=1;i<=n;i++)
		if(dcc[i]==0){
			Ans.push_back(vector<int>());
			dfs(i,++ans);
		}
	cout<<ans<<"\n";
	for(int i=0;i<ans;i++){
		cout<<Ans[i].size()<<" ";
		for(int j=0;j<Ans[i].size();j++)cout<<Ans[i][j]<<" ";
		cout<<"\n";
	}
}

这是模板题的代码,由于题目中的图不是简单图,有自环或者是重边,所以就用链式前向星了。

注:图有可能不连通,所以需要多次dfs

模板题:【模板】边双连通分量

点双连通分量:

首先,两个点双最多只有一个公共点,且这个点在这两个点双和它形成的子图中是割点。

因为当它们有两个及以上公共点时,它们可以合并为一个新的点双。

所以,割点属于多个点双。

我们这里要使用栈,首先按照dfs遍历顺序依次把节点弹入栈中,然后回溯的时候,若发现割点,那么就弹出栈中的节点,直至弹到此割点(注意,割点不弹出,因为割点属于多个点双连通分量)

然后弹出的一大堆东西就是点双连通分量。

注意,如果弹到根节点,那么不管它是不是割点,一律出栈。

这样子,我们就求出来了一个无向图中的所有点双。

(其实点双和scc强连通分量的思路有点像)

CODE:

#include<bits/stdc++.h>
using namespace std;
int n,m,low[500005],dfn[500005],ans,cnt,nxt[4000005],head[500005],go[4000005],k;
vector<int> dcc[500005];
stack<int>sta;
void add(int u,int v){
	nxt[++k]=head[u];
	head[u]=k;
	go[k]=v;
}
void tarjan(int x,int root){
	dfn[x]=low[x]=++cnt;
	if(x==root&&!head[x])dcc[++ans].push_back(x);
	sta.push(x);
	for(int i=head[x];i;i=nxt[i]){
		int g=go[i];
		if(!dfn[g]){
			tarjan(g,root);
			low[x]=min(low[x],low[g]);
			if(low[g]>=dfn[x]){
				ans++;
				int p;
				do{
					p=sta.top();
					sta.pop();
					dcc[ans].push_back(p);
				}while(p!=g);
				dcc[ans].push_back(x);
			}
		}
		else low[x]=min(low[x],dfn[g]);
	}
}
int main(){
	cin>>n>>m;
	for(int i=1,x,y;i<=m;i++){
		cin>>x>>y;
		if(x==y)continue;
		add(x,y);
		add(y,x);
	}
	for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i,i);
	cout<<ans<<"\n";
	for(int i=1;i<=ans;i++){
		cout<<dcc[i].size()<<" ";
		for(int j=0;j<dcc[i].size();j++)cout<<dcc[i][j]<<" ";
		cout<<"\n";
	}
}

那么点双有什么用处呢?

我们可以用它来搭建圆方树,来解决一下查询的问题,这里就不一一赘述了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值