AcWing 算法提高课 无向图的双联通分量
一、定义:
1、极大的不含有桥(割边)的连通块称为边双连通分量。
性质:
(1)边双连通分量,不管删掉哪条边,还是连通的
(2)任意两点间都有两条(边)不相交的路径
2、极大的不含有割点的连通块称为点双连通分量。
性质:
(1)每个割点至少属于两个点双连通分量
(2)割点和割边没什么关系
二、求解方法
1、Tarjan算法求边双连通分量:
模板:(同时记录桥和双连通分量)

const int N=5010; const int M=20010; vector<int> adj[N]; vector<int> idx[N];//边的编号 int dfn[N],low[N],timestamp; stack<int> stk; int id[N],dcc_cnt; bool is_bridge[M]; int n,m; void Tarjan(int u,int from) { dfn[u]=low[u]=++timestamp; stk.push(u); for(int i=0;i<adj[u].size();i++) { int nxt=adj[u][i]; int e=idx[u][i]; if(!dfn[nxt]) { Tarjan(nxt,e); low[u]=min(low[u],low[nxt]); if(dfn[u]<low[nxt]) { is_bridge[e]=is_bridge[e^1]=true; } } else if(e!=(from^1)) { low[u]=min(low[u],dfn[nxt]); } } if(dfn[u]==low[u]) { ++dcc_cnt; int v; do { v=stk.top(); stk.pop(); id[v]=dcc_cnt; } while(v!=u); } } void YD() { cin>>n>>m; int cnt=0; while(m--) { int a,b; cin>>a>>b; adj[a].pub(b); idx[a].pub(cnt++); adj[b].pub(a); idx[b].pub(cnt++); } Tarjan(1,-1); }
2、Tarjan算法求点双连通分量
(1)求割点
模板:(求割点,并计算去掉割点后,此割点所处的连通块会变成几个块)

int n,m; const int N=10010; vector<int> adj[N]; int dfn[N],low[N],timestamp; int root,ans; void Tarjan(int u) { dfn[u]=low[u]=++timestamp; int cnt=0; for(auto nxt:adj[u]) { if(!dfn[nxt]) { Tarjan(nxt); low[u]=min(low[u],low[nxt]); if(low[nxt]>=dfn[u]) { cnt++; } } else low[u]=min(low[u],dfn[nxt]); } if(u!=root&&cnt>0) { cnt++; } ans=max(ans,cnt); } void YD() { fore(i,1,n) adj[i].clear(); memset(dfn,0,sizeof(dfn)); memset(low,0,sizeof(low)); timestamp=0; while(m--) { int a,b;cin>>a>>b; a++,b++; adj[a].push_back(b); adj[b].push_back(a); } int cnt=0; ans=0; fore(i,1,n) { if(!dfn[i]) { root=i; cnt++; Tarjan(i); } } cout<<ans+cnt-1<<endl; }
Tarjan()中的cnt表示去掉当前割点后会变为几个连通块,YD()中的cnt是连通块的个数
(2)求点双连通分量
点双连通分量的缩点方式:
求点双连通分量的模板:

const int N=1010; int n,m; vector<int> adj[N]; stack<int> stk; int dfn[N],low[N],timestamp; int dcc_cnt; vector<int> dcc[N];//存储双连通分量中的点 bool cut[N]; int root; void Tarjan(int u) { dfn[u]=low[u]=++timestamp; stk.push(u); if(root==u&&adj[u].size()==0) { dcc_cnt++; dcc[dcc_cnt].pub(u); return; } int cnt=0;//分支数 for(auto nxt:adj[u]) { if(!dfn[nxt]) { Tarjan(nxt); low[u]=min(low[u],low[nxt]); if(dfn[u]<=low[nxt]) { cnt++; //根且分支数大于等于2,非根分支数大于等于1,就是割点。 if(u!=root||cnt>1) cut[u]=true; ++dcc_cnt; int v; do { v=stk.top(); stk.pop(); dcc[dcc_cnt].pub(v); } while(v!=nxt);//注意判断条件 dcc[dcc_cnt].pub(u);//还要将当前点放入 } } else { low[u]=min(low[u],dfn[nxt]); } } } void YD() { ii++; timestamp=0;dcc_cnt=0;n=0; while(!stk.empty()) stk.pop(); fore(i,1,N) { adj[i].clear(); dcc[i].clear(); } memset(dfn,0,sizeof(dfn)); memset(low,0,sizeof(low)); memset(cut,0,sizeof(cut)); while(m--) { int a,b;cin>>a>>b; n=max({n,a,b}); adj[a].pub(b); adj[b].pub(a); } for(root=1;root<=n;root++) { if(!dfn[root]) { Tarjan(root); } }
三、例题
1、给定一个无向连通图,问最少加几条边,可以将其变成一个边双连通分量
(1)获取全部边双连通分量
(2)将边双连通分量缩点,此时图上的所有边都是桥,即变为一棵树
(3)度为1的点都需要加一条边,设有cnt个度为1的点,故需要加[cnt/2]条边(上取整),即(cnt+1)/2(整数除法)
例题:https://www.acwing.com/problem/content/397/
代码:

#include<bits/stdc++.h> #define fore(x,y,z) for(LL x=(y);x<=(z);x++) #define forn(x,y,z) for(LL x=(y);x<(z);x++) #define rofe(x,y,z) for(LL x=(y);x>=(z);x--) #define rofn(x,y,z) for(LL x=(y);x>(z);x--) #define pub push_back #define all(x) (x).begin(),(x).end() #define fi first #define se second using namespace std; typedef long long LL; typedef pair<int,int> PII; typedef pair<LL,LL> PLL; const int N=5010; const int M=20010; vector<int> adj[N]; vector<int> idx[N];//边的编号 int dfn[N],low[N],timestamp; stack<int> stk; int id[N],dcc_cnt; bool is_bridge[M]; int n,m; int d[N];//缩点后的度数 void Tarjan(int u,int from) { dfn[u]=low[u]=++timestamp; stk.push(u); for(int i=0;i<adj[u].size();i++) { int nxt=adj[u][i]; int e=idx[u][i]; if(!dfn[nxt]) { Tarjan(nxt,e); low[u]=min(low[u],low[nxt]); if(dfn[u]<low[nxt]) { is_bridge[e]=is_bridge[e^1]=true; } } else if(e!=(from^1)) { low[u]=min(low[u],dfn[nxt]); } } if(dfn[u]==low[u]) { ++dcc_cnt; int v; do { v=stk.top(); stk.pop(); id[v]=dcc_cnt; } while(v!=u); } } void YD() { cin>>n>>m; int cnt=0; while(m--) { int a,b; cin>>a>>b; adj[a].pub(b); idx[a].pub(cnt++); adj[b].pub(a); idx[b].pub(cnt++); } Tarjan(1,-1); fore(i,1,n) { for(int k=0;k<adj[i].size();k++) { int j=adj[i][k]; int e=idx[i][k]; if(is_bridge[e]) { d[id[i]]++; } } } int res=0; fore(i,1,dcc_cnt) { if(d[i]==1) res++; } cout<<(res+1)/2<<endl; } int main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); int T=1; //cin >> T; while (T--) { YD(); } return 0; }
2、删除一个点之后剩余的连通块最多有多少
(1)求一共有几个连通块
(2)求去掉一个割点最多会产生几个新的连通块
例题:https://www.acwing.com/problem/content/1185/
代码:

#include<bits/stdc++.h> #define fore(x,y,z) for(LL x=(y);x<=(z);x++) #define forn(x,y,z) for(LL x=(y);x<(z);x++) #define rofe(x,y,z) for(LL x=(y);x>=(z);x--) #define rofn(x,y,z) for(LL x=(y);x>(z);x--) #define pub push_back #define all(x) (x).begin(),(x).end() #define fi first #define se second using namespace std; typedef long long LL; typedef pair<int,int> PII; typedef pair<LL,LL> PLL; int n,m; const int N=10010; vector<int> adj[N]; int dfn[N],low[N],timestamp; int root,ans; void Tarjan(int u) { dfn[u]=low[u]=++timestamp; int cnt=0; for(auto nxt:adj[u]) { if(!dfn[nxt]) { Tarjan(nxt); low[u]=min(low[u],low[nxt]); if(low[nxt]>=dfn[u]) { cnt++; } } else low[u]=min(low[u],dfn[nxt]); } if(u!=root&&cnt>0) { cnt++; } ans=max(ans,cnt); } void YD() { fore(i,1,n) adj[i].clear(); memset(dfn,0,sizeof(dfn)); memset(low,0,sizeof(low)); timestamp=0; while(m--) { int a,b;cin>>a>>b; a++,b++; adj[a].push_back(b); adj[b].push_back(a); } int cnt=0; ans=0; fore(i,1,n) { if(!dfn[i]) { root=i; cnt++; Tarjan(i); } } cout<<ans+cnt-1<<endl; } int main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); while (cin>>n>>m,n||m) { YD(); } return 0; }
3、求设置几个出口可以使某一个点坍塌后,其他点依旧可以连通到出口
(1)若只有一个点的连通块,数量加1
(2)否则,求连通块的点双连通分量,此时若此连通块就是一个点双连通分量,即没有割点,需要出口数加2
(2)否则,需要在度为1的点双连通分量(即只有一个割点)内部需要设置一个出口
然后将数量加起来,方案数乘起来。
代码:

#include<bits/stdc++.h> #define fore(x,y,z) for(LL x=(y);x<=(z);x++) #define forn(x,y,z) for(LL x=(y);x<(z);x++) #define rofe(x,y,z) for(LL x=(y);x>=(z);x--) #define rofn(x,y,z) for(LL x=(y);x>(z);x--) #define pub push_back #define all(x) (x).begin(),(x).end() #define fi first #define se second using namespace std; typedef unsigned long long LL; typedef pair<int,int> PII; typedef pair<LL,LL> PLL; int ii=0; const int N=1010; int n,m; vector<int> adj[N]; stack<int> stk; int dfn[N],low[N],timestamp; int dcc_cnt; vector<int> dcc[N];//存储双连通分量中的点 bool cut[N]; int root; void Tarjan(int u) { dfn[u]=low[u]=++timestamp; stk.push(u); if(root==u&&adj[u].size()==0) { dcc_cnt++; dcc[dcc_cnt].pub(u); return; } int cnt=0;//分支数 for(auto nxt:adj[u]) { if(!dfn[nxt]) { Tarjan(nxt); low[u]=min(low[u],low[nxt]); if(dfn[u]<=low[nxt]) { cnt++; //根且分支数大于等于2,非根分支数大于等于1,就是割点。 if(u!=root||cnt>1) cut[u]=true; ++dcc_cnt; int v; do { v=stk.top(); stk.pop(); dcc[dcc_cnt].pub(v); } while(v!=nxt);//注意判断条件 dcc[dcc_cnt].pub(u);//还要将当前点放入 } } else { low[u]=min(low[u],dfn[nxt]); } } } void YD() { ii++; timestamp=0;dcc_cnt=0;n=0; while(!stk.empty()) stk.pop(); fore(i,1,N) { adj[i].clear(); dcc[i].clear(); } memset(dfn,0,sizeof(dfn)); memset(low,0,sizeof(low)); memset(cut,0,sizeof(cut)); while(m--) { int a,b;cin>>a>>b; n=max({n,a,b}); adj[a].pub(b); adj[b].pub(a); } for(root=1;root<=n;root++) { if(!dfn[root]) { Tarjan(root); } } int res=0;//最小个数 LL num=1;//方案数 fore(i,1,dcc_cnt) { int cnt=0; for(auto u:dcc[i]) { if(cut[u]) { cnt++; } } if(cnt==0) { if(dcc[i].size()>1) { res+=2; num*=dcc[i].size()*(dcc[i].size()-1)/2; } else { res++; } } else if(cnt==1) { res++; num*=dcc[i].size()-1; } } cout<<"Case "<<ii<<": "<<res<<' '<<num<<endl; } int main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); while (cin>>m,m) { YD(); } return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人