连通分量

连通分量

在一张连通的无向图中,对于两个点 \(u\)\(v\),如果无论删去哪条边(只能删去一条)都不能使它们不连通,我们就说 \(u\)\(v\) 边双连通

在一张连通的无向图中,对于两个点 \(u\)\(v\) 如果无论删去哪个点(只能删去一个,且不能删 \(u\)\(v\) 自己)都不能使它们不连通,我们就说 \(u\)\(v\) 点双连通

边双具有传递性,点双不具有传递性

割点

对于一个无向图,如果把一个点删除后这个图的极大连通分量数增加了,那么这个点d就是这个图的割点

正因如此,每一个割点都至少存在于两个点双之中

而点双一点包含至少一个割点,甚至有完全由割点组成的点双

割点的求法:

对于某个顶点 \(u\) ,若存在至少一个儿子 \(v\) 使其不能回到 \(u\)\(low_u\ge dfn_v\) 那么 \(u\) 就是割点,有一个特例就是搜索的起始点,必须要两个以上的儿子不能回到 \(u\) 时才是割点

点双连通分量

关于如何求得点双连通分量:

首先要明确一点的是点双连通分量不等于删除割点后图的剩余连通块

而是对于一个无向图中的 极大 边双连通的子图

我们要找的就是所有的这样的子图

我们在进行 tarjan 时维护了一个栈,当 \(low_u\ge dfn_v\) 成立时,无论 \(u\) 是否为根,都应该进行操作:

1.不断弹出节点直至 \(v\)

2.将 \(u\) 和刚刚弹出的所有节点加入一个新的点双之中

同时单独的一个孤立点也是一个点双,虽然这样好像违背了点双的定义但是为了便于操作还是将其单独划出

以下是求割点和点双的代码:

void tj(int u){
	dfn[u]=low[u]=++ts;
	s.push(u);
	if(u==root&&h[u]==0){//独立的点
		dcc[++idx].push_back(u);
		return ;
	}
	int flag=0;
	for(int i=h[u];i;i=e[i].next){
		int v=e[i].to;
		if(!dfn[v]){
			tj(v);
			low[u]=min(low[v],low[u]);
			if(low[v]>=dfn[u]){
				flag++;
				if(u!=root||flag>1){
					//不等于根时只需要一个儿子
					//否则就两个
					 cut[u]=1;
				}
				idx++;
				int x;
				do{//此时在统计当前点属于的联通分量
					x=s.top();
					s.pop();
					dcc[idx].push_back(x);
				}while(x!=v);
				dcc[idx].push_back(u);
			}
		}
		else{
			low[u]=min(low[u],dfn[v]);
		}
	}
}

在进行点双连通分量的统计时一般就像上方代码一样在tarjan找割点时就统计,也可以在标记完所有割点后再使用 dfs 统计,便于进行拓展操作

割边

与割点类似,对于一个无向图,如果删掉一条边后图中的连通分量数增加了,则称这条边为桥或者割边

求法与割点基本相同,只需要将判断修改为 \(low_u> dfn_v\) 就可以了

以下是代码:

void tj(int u,int in){
	dfn[u]=low[u]=++ts;
	for(int i=h[u];~i;i=e[i].next){
		int v=e[i].to;
		if(!dfn[v]){
			tj(v,i);
			if(dfn[u]<low[v]) bri[i]=bri[i^1]=true;
			low[u]=min(low[u],low[v]);
		}
		else if(i!=(in^1)){
			low[u]=min(low[u],dfn[v]);
		}
	}
}

需要注意的是,由于是无向图,所以对边的标记成对出现,我们这里用位运算表示,

同时还为了避免往回走,更新时也做了判断

边双连通分量

对于边双的划分由于其没有涉及到点,所以简单了许多,一条割边的两端就是两个边双

一下是求边双的代码

void dfs(int u,int id){
	 vis[u]=id;
	 bcc[id-1].push_back(u);
	 for(int i=h[u];~i;i=e[i].next){
	 	int j=e[i].to;
	 	if(vis[j]||bri[i]) continue;
	 	dfs(j,id);
	 }
}
for(int i=1;i<=n;i++){
	if(!vis[i]){
		bcc.push_back(vector<int>());
		dfs(i,++ans);
	}
}

了解了割点与桥的求法,我们可以完成例题

P3225 HNOI2012 矿场搭建 - 洛谷

题意

题目要求在某个煤点坍塌后,所有其他煤点的工人都可以有一条道路通往设置的救援出口,求至少设置的出口数与设置的方案数

分析

不难发现坍塌煤点就是要求删除一个点,那我们求出割点,不难发现,对于每个割点的两端都需要一个救援出口,对于割点的两端在删去割点后都是连通的,所以救援出口设置的位置是随便的

所以对于一个点双连通分量:

当没有割点时,建立两个出口,方案数就是 \(size_i*(size_i-1)/2\)

一个割点时,建立一个出口,答案乘上这个连通分量的大小减去 1

两个割点时不需要建立,不管在哪坍塌都可以逃生

特殊情况是整个图只有一个点,那就只能建一个出口(好坑)那么代码就可以写了

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1005;
int n,m;
vector<int> E[N<<1];
int root;
int dfn[N],low[N];
int ts;
stack<int> s;
vector<int> dcc[N];
int dcc_cnt;
int cut[N];
void tj(int u){
	dfn[u]=low[u]=++ts;
	s.push(u);
	if(u==root&&E[u].size()==0){
		dcc_cnt++;
		dcc[dcc_cnt].push_back(u);
		s.pop(); 
		return ;
	}
	int son=0;
	for(auto v:E[u]){
		if(!dfn[v]){
			tj(v);
			low[u]=min(low[u],low[v]);
			if(dfn[u]<=low[v]){
				son++;
				if(u!=root||son>1) cut[u]=1; 
				dcc_cnt++;
				int x;
				do{
					x=s.top();
					s.pop();
					dcc[dcc_cnt].push_back(x);
				}while(x!=v);
				dcc[dcc_cnt].push_back(u);
			}
		}
		else{
			low[u]=min(low[u],dfn[v]);
		}
	}
}
signed main(){
	int T=1;
	while(cin>>m,m){
		for(int i=1;i<=dcc_cnt;i++){
			dcc[i].clear();
		}
		for(int i=1;i<=n;i++){
			E[i].clear();
			dfn[i]=0;
			low[i]=0; 
			cut[i]=0;
		}
		dcc_cnt=n=ts=0; 
		for(int i=1;i<=m;i++){
			int u,v;
			cin>>u>>v;
			E[u].push_back(v);
			E[v].push_back(u);
			n=max({n,u,v});
		}
		for(int i=1;i<=n;i++){
			if(!dfn[i]){
				root=i;
				tj(i);
			}
		}
		int res=0;
		int sum=1;
		for(int i=1;i<=dcc_cnt;i++){
			int flag=0;
			for(int o=0;o<dcc[i].size();o++){
				if(cut[dcc[i][o]]){
					flag++;
				}
			}
			if(flag==0){
				if(dcc[i].size()==1){
					res++;
				}
				else{
					res+=2;
					sum*=(dcc[i].size()*(dcc[i].size()-1))/2;
				}
			}
			else if(flag==1){
				res++;
				sum*=(dcc[i].size()-1);
			}
		}
		cout<<"Case "<<T++<<": "<<res<<" "<<sum<<endl;
	}
	return 0;
}
posted @ 2025-07-25 16:21  Zom_j  阅读(57)  评论(0)    收藏  举报