割点、割边及双联通分量
一、双联通的概念与性质
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;
}
若原图是一个双联通图,则至少要选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;
}

浙公网安备 33010602011771号