割点、割边及双联通分量

一、双联通的概念与性质

1.概念

这里的概念均为无向图中的概念。

无向图的dfs森林中只有两种边: 树边和返祖边。

割边:若删除一条边后,使得原本联通的图变得不联通(分为了两部分),那么这条边就是割边.

割点:若删除一个点和与它有关的所有边后,使得原本联通的图变得不联通,那么这个点就是割点.

边双联通:没有割边,可以理解为必须要删除至少两条边才能使原图变得不联通.

点双联通:没有割点,可以理解为必须要删除至少两个点才能使原图变得不联通.

点双联通图:没有割点的无向联通图。

边双联通图:没有割边的无向联通图。

点双联通分量:极大的点双联通子图。

边双连通分量:极大的边双联通子图。

2.性质

边双联通图的性质

从任意一个点到另一个点都存在两条没有重复的边的简单路径。

点双联通图的性质

从任意一个点到另一个点都存在两条没有重复的点的简单路径。

二、双联通分量缩图

边双联通分量缩图:

把所有边双连通分量缩成一个点。

那么新图中的边都是原图中的割边。

而且新图是一棵树。

点双联通分量缩图:

把所有的点双联通分量缩成一个方点,所有割点看做一个圆点。

连接点双联通分量和其中的割点。

就得到了一颗圆方树。

三、\(Tarjan\)算法

求强联通分量的tarjan算法类似。

但要注意转移时略有不同。

注意如何判重边。

求割边的算法流程:

枚举点\(u\),枚举出边\((u,v)\)

1.若\(v\)未访问过,则\(dfs(v)\),更新\(low[u]=min(low[u],low[v]\).

同时如果\(low[v]>dfn[u]\),那么\((u,v)\)这条边就是割边.

2.若\(v\)已访问过且\(v\)不是\(u\)的父亲(\(v\)\(u\)的祖先),则更新\(low[u]=min(low[u],dfn[v])\).

如果一个点\(u\)满足\(dfn[u]=low[u]\),将\(u\)以后得点出栈,就是一个边双连通分量。

code(求割边)
inline void dfs(int x,int pre_id){
	dfn[x]=low[x]=++cnt;
	for(auto to : E[x]){
		int v=to.v,id=to.id;
		if(!dfn[v]){
			dfs(v,id);
			low[x]=min(low[x],low[v]);
			if(low[v]>dfn[x]) bridge.push_back(id);
		}
		else if(id!=pre_id) low[x]=min(low[x],dfn[v]);
	}
	return ;
}
code(求边双连通分量)
inline void dfs(int x,int pre_id){
	dfn[x]=low[x]=++cnt;
	stk[++top]=x;
	for(auto to : E[x]){
		int v=to.v,id=to.id;
		if(!dfn[v]){
			dfs(v,id);
			low[x]=min(low[x],low[v]);
		}
		else if(id!=pre_id)
			low[x]=min(low[x],dfn[v]);
	}
	if(dfn[x]==low[x]){
		tot++;
		while(1){
			int v=stk[top]; top--;
			dcc[tot].push_back(v);
			if(v==x) break;
		}
	}
	return ;
}

求割点的算法流程:

枚举点\(u\),枚举出边\((u,v)\)

1.若\(v\)未访问过,则\(dfs(v)\),更新\(low[u]=min(low[u],low[v]\).

同时如果\(low[v]>=dfn[u]\),那么\(u\)就是割点.

如果求点双联通分量,就在这里将\(v\)以后的点出栈,在加上割点\(u\)就是一个点双联通分量。

2.若\(v\)已访问过且\(v\)不是\(u\)的父亲(\(v\)\(u\)的祖先),则更新\(low[u]=min(low[u],dfn[v])\)(一定要这么写,否则会出问题).

注意:需要特判根节点:只有满足儿子数量\(>1\)的根节点才是割点.

code(求割点)
inline void dfs(int x,int pre_id){
	dfn[x]=low[x]=++cnt;
	for(auto to : E[x]){
		int v=to.v,id=to.id;
		if(!dfn[v]){
			dfs(v,id);
			low[x]=min(low[x],low[v]);
			siz[x]++;
			if(low[v]>=dfn[x]) d[x]++;
		}
		else if(id!=pre_id)
			low[x]=min(low[x],dfn[v]);
	}
	if(!pre_id && siz[x]<=1) d[x]=0;
	if(d[x]) dot.push_back(x);
	return ;
}
code(求点双联通分量)
inline void dfs(int x,int pre_id){
	dfn[x]=low[x]=++cnt;
	stk[++top]=x;
	int siz=0;
	for(auto to : E[x]){
		int v=to.v,id=to.id;
		if(!dfn[v]){
			dfs(v,id); siz++;
			low[x]=min(low[x],low[v]);
			if(low[v]>=dfn[x]){
				tot++;
				while(1){
					int w=stk[top]; --top;
					dcc[tot].push_back(w);
					if(w==v) break;
				}
				dcc[tot].push_back(x);
			}
		}
		else if(id!=pre_id)
			low[x]=min(low[x],dfn[v]);
	}
	if(!pre_id && !siz){//特判单点
		tot++;
		dcc[tot].push_back(x);
	}
	return ;
}

四、题集

1.P2860 [USACO06JAN] Redundant Paths G

给一个\(n\)个点\(m\)条边的无向连通图,问最少添加多少条边使得成为一个边双连通图。

可以发现添加边时一定是要用一条边与其他几条割边组成一个环,这样就消除了这几条割边。

可以先缩图,将原图缩成一棵树,则问题转化为:

求每次选两个点,将它们之间的路径覆盖一次,使得这棵树上的每一条边都覆盖至少一次的最小操作次数。

而这个问题有一个结论:设叶子节点个数为\(l\),最少次数为\(\frac{l+1}{2}\).

说明

一个可行的构造方法:

将所有叶子节点从左至右依次编号为\(1,2,3...,l\)

则第一次覆盖\((1,\frac{l}{2}+1)\).

第二次覆盖\((2,\frac{l}{2}+2)\).

依此类推...

则这样就可以花费\(\frac{l+1}{2}\)c次操作来解决这个问题。

code

#include <bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define inf 1e10
#define eps 1e-9
#define endl "\n"
#define il inline
#define ls 2*k
#define rs 2*k+1
using namespace std;
const int N=2e5+5,M=1e7+5;
inline int read(){
    int x=0,f=1;
    char ch=getchar();
    while(ch<'0'||ch>'9'){
        if(ch=='-') f=-1;
        ch=getchar();
    }
    while(ch>='0' && ch<='9') x=x*10+ch-'0',ch=getchar();
    return x*f;
}
int n,m,cnt,tot,top,d[N];
int dfn[N],low[N],stk[N],bel[N];
struct edge{ int v,id; };
vector<edge>E[N];
inline void dfs(int x,int pre_id){
	dfn[x]=low[x]=++cnt;
	stk[++top]=x;
	for(auto to : E[x]){
		int v=to.v,id=to.id;
		if(!dfn[v]){
			dfs(v,id);
			low[x]=min(low[x],low[v]);
		}
		else if(id!=pre_id)
			low[x]=min(low[x],dfn[v]);
	}
	if(dfn[x]==low[x]){
		++tot;
		while(top){
			int v=stk[top]; --top;
			bel[v]=tot;
			if(v==x) break;
		}
	}
	return ;
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin>>n>>m;
	for(int i=1,u,v;i<=m;i++){
		cin>>u>>v;
		E[u].push_back({v,i});
		E[v].push_back({u,i});
	}
	for(int i=1;i<=n;i++)
		if(!dfn[i]) dfs(i,0);
	for(int i=1;i<=n;i++)
		for(auto to : E[i]){
			int v=to.v,id=to.id;
			if(bel[v]==bel[i]) continue;
			d[bel[v]]++;
		}
	int ans=0;
	for(int i=1;i<=tot;i++){
		if(d[i]==1) ans++;
//		cout<<i<<' '<<d[i]<<'\n';
	}
	cout<<(ans+1)/2;
	return 0;
}
/*
缩图后形成一棵树. 
*/

2.P3469 [POI 2008] BLO-Blockade

\(n\)个点\(m\)条边无向连通图的图,保证没有重边和自环。对于每个点,输出将这个点的所有边删除之后,有多少点对不能互相连通。这里的点对是有顺序的,也就是\((u,v)\)\((v,u)\)需要被统计两次。

可以发现除了割点以外的点的答案均为\(2(n-1)\).

而对于割点,如果它的一颗子树只能通过此割点来到达其他的点,设子树大小为\(siz\),则答案会增加\(2siz(n-siz-1)\),同时注意算过的不要重复算.

code

#include <bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define inf 1e10
#define eps 1e-9
#define endl "\n"
#define il inline
#define ls 2*k
#define rs 2*k+1
using namespace std;
const int N=5e5+5,M=1e7+5;
inline int read(){
    int x=0,f=1;
    char ch=getchar();
    while(ch<'0'||ch>'9'){
        if(ch=='-') f=-1;
        ch=getchar();
    }
    while(ch>='0' && ch<='9') x=x*10+ch-'0',ch=getchar();
    return x*f;
}
int n,m,cnt,tot,top,siz[N];
int dfn[N],low[N],stk[N],bel[N],ans[N];
struct edge{ int v,id; };
vector<edge>E[N];
inline void dfs(int x,int pre_id){
	dfn[x]=low[x]=++cnt;
	ans[x]=2*(n-1);siz[x]=1;
	int ch=0,now=0;
	for(auto to : E[x]){
		int v=to.v,id=to.id;
		if(!dfn[v]){
			dfs(v,id);
			siz[x]+=siz[v];ch++;
			low[x]=min(low[x],low[v]);
			if(low[v]>=dfn[x])
				now+=siz[v],ans[x]=ans[x]+siz[v]*(n-now-1)*2;
		}
		else if(id!=pre_id)
			low[x]=min(low[x],dfn[v]); 
	}
	if(!pre_id && ch<=1) ans[x]=2*(n-1);
	return ;
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin>>n>>m;
	for(int i=1,u,v;i<=m;i++){
		cin>>u>>v;
		E[u].push_back({v,i});
		E[v].push_back({u,i});
	}
	for(int i=1;i<=n;i++)
		if(!dfn[i]) dfs(i,0);
	for(int i=1;i<=n;i++) cout<<ans[i]<<'\n';
	return 0;
}

3.P3225 [HNOI2012] 矿场搭建

若原图是一个双联通图,则至少要选2个点,方案数为\(\frac{n(n-1)}{2}\).

否则的话,先缩图,对于只存在一个割点的点双联通分量,这个联通分量中一定要有一个点被选中(否则把割点炸了,就废了)。

最终的方案数将各个需要选的联通分量的点数相乘即可。

code

#include <bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define inf 1e10
#define eps 1e-9
#define endl "\n"
#define il inline
#define ls 2*k
#define rs 2*k+1
using namespace std;
const int N=1005,M=1e7+5;
inline int read(){
    int x=0,f=1;
    char ch=getchar();
    while(ch<'0'||ch>'9'){
        if(ch=='-') f=-1;
        ch=getchar();
    }
    while(ch>='0' && ch<='9') x=x*10+ch-'0',ch=getchar();
    return x*f;
}
bool cut[N];
int n=1000,m,cnt,tot,top;
int dfn[N],low[N],d[N],stk[N],siz[N];
struct edge{ int v,id; };
vector<edge>E[N];
vector<int>dcc[N];
inline void dfs(int x,int pre_id,int fa){
	dfn[x]=low[x]=++cnt;
	stk[++top]=x;
	int ch=0,res=0;
	for(auto to : E[x]){
		int v=to.v,id=to.id;
		if(!dfn[v]){
			dfs(v,id,x);ch++;
			low[x]=min(low[x],low[v]);
			if(low[v]>=dfn[x]){
				++tot;res++;
				while(true){
					int w=stk[top]; top--;
					dcc[tot].push_back(w);
					siz[tot]++;
					if(w==v) break;
				}
				siz[tot]++,dcc[tot].push_back(x);
			}
		}
		else if(id!=pre_id)
			low[x]=min(low[x],dfn[v]);
	}
	if(!pre_id && ch<=1) res=0;
	if(res) cut[x]=1;
	return ;
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	int T=0;
	while(++T){
		cin>>m;
		if(m==0) exit(0);
		for(int i=1,u,v;i<=m;i++){
			cin>>u>>v;
			E[u].push_back({v,i});
			E[v].push_back({u,i});
		}
		
		for(int i=1;i<=n;i++)
			if(!dfn[i]) dfs(i,0,0);
		
		for(int i=1;i<=tot;i++)
			for(auto u : dcc[i])
				if(cut[u]) d[i]++;
		if(tot==1)
			cout<<"Case "<<T<<": "<<2<<' '<<siz[tot]*(siz[tot]-1)/2<<'\n'; 
		else{
			int ans=0,way=1;
			for(int i=1;i<=tot;i++)
				if(d[i]==1) ans++,way=way*(siz[i]-1);
			cout<<"Case "<<T<<": "<<ans<<' '<<way<<'\n'; 		
		}
		for(int i=1;i<=n;i++) E[i].clear(),dfn[i]=siz[i]=low[i]=d[i]=cut[i]=0;
		for(int i=1;i<=tot;i++) dcc[i].clear(); cnt=top=tot=0;
	}
	return 0;
}

posted @ 2026-01-28 13:48  Lmx__qwq  阅读(2)  评论(0)    收藏  举报