割点割边双连通分量

一.双连通分量,割点,割边

割点定义:对于一个连通图,如果删去这个点后,会存在两个及两个以上的连通图

割边定义:把一条边删掉后,这个图会被分割成两个部分,又称桥

双连通概念:分为点双连通分量和边双连通分量

点双连通:没有割点

边双连通:没有割边

双连通的性质:

对于点:对于任意两点u,v,都存在两条简单路径(简单路径不经过重复点),这两条简单路径点不相交(两条路径上的点都互不相同)

对于边:对于任意两点u,v,都存在两条简单路径,这两条简单路径边不相交

不难看出,一个图如果是点双,那么这个图也一定是边双

双连通分量:

抽象的定义:极大的点集,满足导出子图是点(或边)是双连通的
点双实例:(一个点可能在多个连通分量内)

边双实例:

双连通分量缩图

边双:树
点双:圆方树(block tree)

二.tarjan算法求双连通分量

1.割边(无向图)

DFS的话?只有返祖边和树边,假设有横叉边,那么横叉边一定是指向前面被遍历过的,而被遍历过的点,一个会遍历到这个横叉边,所以就矛盾了,故而不存在横叉边
首先返祖边一定不是割边,因为返祖边会形成一个环,删去后,图仍然连通
然后考虑树边,如果这条树边是割边的话,那么以这个点为根的子树就没有边返祖上去
所以用tarjan就行了(dfn,low)
例题:https://www.luogu.com.cn/problem/P1656
需要注意的是重边!

#include<bits/stdc++.h>

#define x first
#define y second
#define endl '\n'
#define int long long
 
using namespace std;

const int N=1e6+10; 

typedef pair<int,int> PII;

vector<int> e[N]; 
int dfn[N],low[N],idx,n,m;
vector<PII> bridge; 

void dfs(int u,int fa){
	dfn[u]=low[u]=++idx;	
	bool ok=false;//特判连向父亲节点的重边
	for(auto v:e[u]){
		if(!dfn[v]){
			dfs(v,u);
		}
		if(v!=fa||ok) low[u]=min(low[u],low[v]);
		if(v==fa) ok=true;
	} 
	if(dfn[u]==low[u]&&fa!=-1){//找到了一个桥 
		bridge.push_back({min(u,fa),max(u,fa)});
	}
}

void slove(){
	int n,m;cin>>n>>m;
	for(int i=1;i<=m;i++){
		int u,v;cin>>u>>v;
		e[u].push_back(v);
		e[v].push_back(u);
	}
	dfs(1,-1);
	sort(bridge.begin(),bridge.end());
	for(auto zz:bridge) cout<<zz.x<<" "<<zz.y<<endl;
}

signed main(){
	ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
	int T=1;
//	cin>>T;
	while(T--) slove();
}

2.割点(无向图)

一个点u,若存在子树的返祖边最高就是u,那么u这个点就是割点
注意特殊情况:如果是根,并且只有一个儿子节点,那么他一定不是割点

#include<bits/stdc++.h>

#define x first
#define y second
#define endl '\n'
#define int long long
 
using namespace std;

const int N=1e6+10; 

typedef pair<int,int> PII;

vector<int> e[N]; 
int dfn[N],low[N],idx,n,m,cnt[N];
int sz,root;

//id表示u的父亲->u的这条边的编号 
void dfs(int u,int fa){
	dfn[u]=low[u]=++idx;
	int ch=0;//儿子的个数 
	for(auto v:e[u]){
		if(!dfn[v]){
			dfs(v,u);
			ch++;
			low[u]=min(low[u],low[v]);
			if(low[v]>=dfn[u]) cnt[u]=1;//v跳不出去了 
		}else if(v!=fa){//返祖边,割点的话跟割边是不一样的,只能跳一次,并且不用判断与父亲节点的重边,因为这个点删掉之后,与父亲节点所连的边全部被删
			low[u]=min(low[u],dfn[v]);
		}
	} 
	if(u==root&&ch==1) cnt[u]=0;
	sz+=cnt[u];
}

void slove(){
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int u,v;cin>>u>>v;
		e[u].push_back(v);
		e[v].push_back(u);
	}
	
	for(int i=1;i<=n;i++) if(!dfn[i]) root=i,dfs(i,-1);
	
	cout<<sz<<endl;
	for(int i=1;i<=n;i++)if(cnt[i]){
		cout<<i<<" ";
	} 
}

signed main(){
	ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
	int T=1;
//	cin>>T;
	while(T--) slove();
}

割边例题


思路:边双连通分量缩图就是一棵树,只需要考虑在这棵树上如何加边即可,而每次加边,就相当于把一个路径上的割边全部弄成非割边
首先树上的边,一定都是割边,然后要考虑的就是,加完边后,如果该树边被纳入到一个环内,那么就不是割边了
通过观察可以发现的就是,这题答案就是(叶子个数+1)/2;
题外话:有一个经典的构造,假设有m个叶子节点,那么要选至少多少条路径才能覆盖整颗树的节点?
按照DFS序给每个叶子节点赋个编号,然后就是i->i+m/2这样的形式选路径即可,最终需要的路径路径数量就是(m+1)/2;

#include<bits/stdc++.h>

#define x first
#define y second
#define endl '\n'
#define int long long
 
using namespace std;

const int N=1e6+10; 

typedef pair<int,int> PII;

vector<int> e[N]; 
int dfn[N],low[N],ins[N],bel[N],idx,n,m,cnt;
vector<int> cc[N];
stack<int> stk;

void dfs(int u,int fa){
	dfn[u]=low[u]=++idx;	
	ins[u]=true;
	stk.push(u);
	bool ok=false;
	for(auto v:e[u]){
		if(!dfn[v]){
			dfs(v,u);
		}
		if(v!=fa||ok) low[u]=min(low[u],low[v]);
		if(v==fa) ok=true;
	} 
	if(dfn[u]==low[u]){//找到了一个桥,那么栈里面的点,直到u,都是属于同一个边双连通分量,类似于强连通分量的缩点处理 
		++cnt;
		while(true){
			int v=stk.top();
			cc[cnt].push_back(v);//边双分量 
			ins[v]=false;
			bel[v]=cnt;
			stk.pop();
			if(v==u) break;
		}
	}
}

void slove(){
	int n,m;cin>>n>>m;
	for(int i=1;i<=m;i++){
		int u,v;cin>>u>>v;
		e[u].push_back(v);
		e[v].push_back(u);
	}
	dfs(1,-1);
	int nleaf=0;
	for(int i=1;i<=cnt;i++){
		int cnte=0;
		for(auto u:cc[i]){
			for(auto v:e[u])if(bel[u]!=bel[v]) cnte++;//割边 
		}
		if(cnte==1) nleaf++;//叶子节点连向的割边只有一条 
	} 
//	cout<<nleaf<<endl;
	cout<<(nleaf+1)/2<<endl;
}

signed main(){
	ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
	int T=1;
//	cin>>T;
	while(T--) slove();
}

例题2

https://www.luogu.com.cn/problem/P3469

#include<bits/stdc++.h>

#define x first
#define y second
#define endl '\n'
#define int long long
 
using namespace std;

const int N=1e6+10; 

typedef pair<int,int> PII;

vector<int> e[N]; 
int dfn[N],low[N],idx,n,m,sz[N],ans[N];

//id表示u的父亲->u的这条边的编号 
void dfs(int u,int fa){
	dfn[u]=low[u]=++idx;
	sz[u]=1;
	ans[u]=n-1;//被删除掉的点u,跟其他点都一定不连通
	int cut=n-1;//剩余的连通的点的数量
	for(auto v:e[u]){
		if(!dfn[v]){
			dfs(v,u);
			sz[u]+=sz[v];
			low[u]=min(low[u],low[v]);
			if(low[v]>=dfn[u]){
				ans[u]+=sz[v]*(n-sz[v]);//把u删掉后,以v为根的这颗子树与其他的节点一定不连通 
				cut-=sz[v];
			}
		}else if(v!=fa){
			low[u]=min(low[u],dfn[v]);
		}
	} 
	ans[u]+=cut*(n-cut);//上面的连通部分 
}

void slove(){
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int u,v;cin>>u>>v;
		e[u].push_back(v);
		e[v].push_back(u);
	}
	
	dfs(1,-1);
	
	for(int i=1;i<=n;i++)cout<<ans[i]<<endl;
}

signed main(){
	ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
	int T=1;
//	cin>>T;
	while(T--) slove();
}

例题三(求出每个具体的点双连通分量板子)

https://www.luogu.com.cn/problem/P3225

1.首先考虑没有割点的情况
至少设置两个救援出口,因为会出现一个救援出口坍塌的情况,方案数为c[n][2]
2.有割点的情况
首先必须在叶子节点设立一个救援出口,因为若叶子节点连向的割点塌了的话,那么这个叶子节点就无路可走了
所以对于每个点连通分量而言,选择一个叶子节点即可

对于这张图来说:
点双集合为
1 3 2
3 4 5 6
3 8 7
8 9

#include<bits/stdc++.h>

#define x first
#define y second
#define endl '\n'
#define int long long
 
using namespace std;

const int N=1e6+10; 

typedef pair<int,int> PII;

vector<int> e[N]; 
int dfn[N],low[N],idx,n,m,cnt,cut[N];
stack<int> stk;
vector<int> cc[N]; 

/*
相当于求出每个点双连通分量的板子了 
*/
void dfs(int u,int fa){
	dfn[u]=low[u]=++idx;
	stk.push(u);
	
	int ch=0;
	for(auto v:e[u]){ 
		if(!dfn[v]){
			dfs(v,u);
			ch++;
			low[u]=min(low[u],low[v]);
			if(low[v]>=dfn[u]){//以u为根节点,并且u是割点,并且子树是以v为根节点的点双连通分量,然后求出一个点双连通分量(每个点双连通分量都有一个割点)
				cut[u]=1;
				++cnt;
				cc[cnt].push_back(u);
				while(true){
					int w=stk.top();
					cc[cnt].push_back(w);
					stk.pop();
					if(w==v) break;
				}
			}
		}else if(v!=fa){
			low[u]=min(low[u],dfn[v]); 
		}
	} 
	if(u==1&&ch<=1) cut[u]=0;
}

void slove(int u,int ca){
	m=u;
	for(int i=1;i<=1000;i++) e[i].clear();
	int n=0;
	for(int i=0;i<m;i++){
		int u,v;cin>>u>>v;
		e[u].push_back(v);
		e[v].push_back(u);
		n=max(n,max(u,v));
	}
	for(int i=1;i<=n;i++){
		dfn[i]=low[i]=cut[i]=0;
		idx=cnt=0;
		cc[i].clear();
	}
	while(!stk.empty())stk.pop();
	
	for(int i=1;i<=n;i++)if(!dfn[i])dfs(i,0);
	
	cout<<"Case "<<ca<<": ";
	if(cnt==1){//只有一个点双,说明所有点都是双连通的 
		int n=cc[1].size();
		cout<<2<<" "<<n*(n-1)/2<<endl;
	}else {
		int ans1=0,ans2=1;
		for(int i=1;i<=cnt;i++){
			int ncut=0;
			for(auto u:cc[i]) ncut+=cut[u];//找割点,一个边双连通分量最多只有一个割点 
			if(ncut==1){
				ans1+=1;
				ans2*=(int)cc[i].size()-1;
			}
		}
		cout<<ans1<<" "<<ans2<<endl; 
	}
}

signed main(){
	ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
	int T=1;
	cin>>T;
	int p=1; 
	while(T!=0){
		slove(T,p++);
		cin>>T;
	}
}
posted @ 2024-12-12 19:14  MENDAXZ  阅读(10)  评论(0编辑  收藏  举报