割点、割边、双连通分量
割点、割边
割点 和 割边 就是特定的点和边,满足在图中删去其后,会使得图的连通分支数增加
例如下图中点 0, 1, 5, 7 就是割点(图中加粗),边 12、05、78、79 就是割边
求割点、割边
求割点 - tarjan
对于无向图的 dfs 森林,如果某个节点 u 的子树及其后代最高仅能到达节点 u 及以下时,节点 u 为割点
特殊情况:当节点 u 为根节点且子树个数等于 1 时不是割点
具体代码写法很多,只要能把每一点最早能抵达的位置计算清楚即可
int dfn[maxm], low[maxm], idx = 0, root, sz; bool cut[maxm]; void tarjan(int u){ dfn[u] = low[u] = ++ idx; int child = 0; for(auto v : e[u]){ if(!dfn[v]){ tarjan(v); low[u] = min(low[u], low[v]); if(low[v] >= dfn[u]){ ++ child; if(u != root || child > 1) cut[u] = true; } }else low[u] = min(low[u], dfn[v]); } if(cut[u]) ++ sz; return ; }
int dfn[maxm], low[maxm], idx = 0, root; bool cut[maxm]; void tarjan(int u, int fa){ dfn[u] = low[u] = ++ idx; int child = 0; for(auto v : e[u]){ if(!dfn[v]){ tarjan(v, u); ++ child; low[u] = min(low[u], low[v]); if(low[v] >= dfn[u]) cut[u] = true; }else if(v != fa) low[u] = min(low[u], dfn[v]); } if(u == root && child < 2) cut[u] = false; return ; }
求割边 - tarjan
对于无向图的 dfs 森林,如果某个节点 u 的支路 v 及 v 的后代只能连通到 v,那么边
具体代码写法很多,只要能把每一点最早能抵达的位置计算清楚即可
例:
int dfn[maxm], low[maxm], idx = 0; void tarjan(int u, int x){ dfn[u] = low[u] = ++ idx; for(auto [v, id] : e[u]){ if(!dfn[v]){ tarjan(v, id); low[u] = min(low[u], low[v]); if(low[v] > dfn[u]){//顶点 v 没有别的方式抵达 u 的上方 ans.push_back({u, v}); } }else if(x != id)//防止同一条边回跳 low[u] = min(low[u], dfn[v]); } return ; }
双连通分量
双连通分为点双连通和边双连通
点双连通:在一个连通图中任选两点,如果它们之间至少存在两条“点不重复”的路径,称为点双连通
边双连通:在一个连通图中任选两点,如果它们之间至少存在两条“边不重复”的路径,称为边双连通
点双连通图可以理解为没有割点的连通图,边双连通可以理解为没有割边的连通图
双连通分量
点双连通分量:一个图中的点双连通极大子图
边双连通分量:一个图中的边双连通极大子图
双连通分量可以缩图,边双缩成树,点双缩成 block tree
相关资料
割点
https://www.cnblogs.com/dx123/p/16320481.html
割边
https://www.cnblogs.com/dx123/p/16320483.html
例题
割点
求割点 - 洛谷 P3388 【模板】割点(割顶)
//>>>Qiansui #include<bits/stdc++.h> #define ll long long #define ull unsigned long long #define mem(x,y) memset(x, y, sizeof(x)) #define debug(x) cout << #x << " = " << x << '\n' #define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << '\n' //#define int long long using namespace std; typedef pair<int, int> pii; typedef pair<ll, ll> pll; typedef pair<ull, ull> pull; typedef pair<double, double> pdd; /* */ const int maxm = 2e4 + 5, inf = 0x3f3f3f3f, mod = 998244353; int n, m; vector<int> e[maxm]; int dfn[maxm], low[maxm], idx = 0, root, sz; bool cut[maxm]; void tarjan(int u){ dfn[u] = low[u] = ++ idx; int child = 0; for(auto v : e[u]){ if(!dfn[v]){ tarjan(v); low[u] = min(low[u], low[v]); if(low[v] >= dfn[u]){ ++ child; if(u != root || child > 1) cut[u] = true; } }else low[u] = min(low[u], dfn[v]); } if(cut[u]) ++ sz; return ; } void solve(){ cin >> n >> m; for(int i = 0; i < m; ++ i){ int u, v; cin >> u >> v; e[u].push_back(v); e[v].push_back(u); } for(root = 1; root <= n; ++ root){ if(!dfn[root]) tarjan(root); } cout << sz << '\n'; for(int i = 1; i <= n; ++ i){ if(cut[i]) cout << i << " "; } return ; } signed main(){ ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr); int _ = 1; // cin >> _; while(_ --){ solve(); } return 0; }
割点运用 - 洛谷 P3469 [POI2008] BLO-Blockade
题意
给定无向连通图,保证没有自环和重边,问你对于图中每一个点,输出删去这个点的所有边后,图中有多少有序点对无法连通
思路
当图中节点 i 不是割点时,答案即为
当图中节点 i 是割点时,删除其所有边会将图划分为 k 个连通块和 i 本身,那么每一个连通块均与剩余的点产生贡献。设 tarjan 求割点时节点 i 及其子树包含的点数为 cut(与下方代码中的 cut 表示意义不同),
具体细节见代码
代码
//>>>Qiansui #include<bits/stdc++.h> #define ll long long #define ull unsigned long long #define mem(x,y) memset(x, y, sizeof(x)) #define debug(x) cout << #x << " = " << x << '\n' #define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << '\n' //#define int long long using namespace std; typedef pair<int, int> pii; typedef pair<ll, ll> pll; typedef pair<ull, ull> pull; typedef pair<double, double> pdd; /* */ const int maxm = 1e5 + 5, inf = 0x3f3f3f3f, mod = 998244353; int n, m; vector<int> e[maxm]; int dfn[maxm], low[maxm], sz[maxm], idx = 0, root; ll ans[maxm]; void tarjan(int u){ dfn[u] = low[u] = ++ idx; int cut = n - 1, child = 0; sz[u] = 1; ans[u] = n - 1;//初始本身与剩余 n - 1 个点对于答案的贡献 for(auto v : e[u]){ if(!dfn[v]){ tarjan(v); sz[u] += sz[v]; low[u] = min(low[u], low[v]); if(low[v] >= dfn[u]){ ++ child; if(u != root || child > 1){//是割点,会对ans[u]产生贡献 ans[u] += (ll) sz[v] * (n - sz[v]); cut -= sz[v]; } } }else low[u] = min(low[u], dfn[v]); } if(cut) ans[u] += (ll) cut * (n - cut);//还有点没被切去 return ; } void solve(){ cin >> n >> m; for(int i = 0; i < m; ++ i){ int u, v; cin >> u >> v; e[u].push_back(v); e[v].push_back(u); } root = 1; tarjan(root); for(int i = 1; i <= n; ++ i){ cout << ans[i] << "\n"; } return ; } signed main(){ ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr); int _ = 1; // cin >> _; while(_ --){ solve(); } return 0; }
vDCC缩点 - 洛谷 P3225 [HNOI2012] 矿场搭建
题意
给定无向连通图。决定在某些点处设置救援点,使得当任何节点崩溃后(当前节点及其所有边均删去),其余所有的点都能抵达某一个救援点。
问你最少的救援点设置数和最小时的方案数
思路
题目输入较为不人性化,所以记得给数组啥的赋初值
对于原图上的点,如果这个点不是割点,意味着删去这个点,原图依旧连通,所以只要删去任意非割点时原图得保证均有至少一个救援点;如果这个点是割点,那个原图划分成的多个连通块中,每个连通块都需要有至少一个救援点
利用 vDCC 缩点得到每个整个图的所有点双分量
如果整个图是点联通的,那么
否则,我们需要在每个叶子节点处设置一个救援点,仅含有一个割点的点双分量就是叶子节点,而救援点的安放就是当前点双分量中处割点外的任意一点即可
代码
//>>>Qiansui #include<bits/stdc++.h> #define ll long long #define ull unsigned long long #define mem(x,y) memset(x, y, sizeof(x)) #define debug(x) cout << #x << " = " << x << '\n' #define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << '\n' //#define int long long using namespace std; typedef pair<int, int> pii; typedef pair<ll, ll> pll; typedef pair<ull, ull> pull; typedef pair<double, double> pdd; /* */ const int maxm = 1e3 + 5, inf = 0x3f3f3f3f, mod = 998244353; int n, m; vector<int> e[maxm]; int dfn[maxm], low[maxm], idx, root, cnt; bool cut[maxm]; stack<int> stk; vector<int> cc[maxm]; void tarjan(int u, int fa){ dfn[u] = low[u] = ++ idx; stk.push(u); int child = 0; for(auto v : e[u]){ if(!dfn[v]){ tarjan(v, u); low[u] = min(low[u], low[v]); ++ child; if(low[v] >= dfn[u]){ cut[u] = true; ++ cnt; cc[cnt].push_back(u); int w; do{ w = stk.top(); stk.pop(); cc[cnt].push_back(w); }while(v != w); } }else if(v != fa) low[u] = min(low[u], dfn[v]); } if(u == root && child <= 1) cut[u] = false; return ; } void solve(){ int T = 0; while(cin >> m){ if(m == 0) break; n = 0; for(int i = 1; i <= maxm; ++ i){ e[i].clear(); cc[i].clear(); } for(int i = 0; i < m; ++ i){ int u, v; cin >> u >> v; n = max(n, max(u, v)); e[u].push_back(v); e[v].push_back(u); } for(int i = 1; i <= n; ++ i){ dfn[i] = low[i] = 0; cut[i] = false; } cnt = idx = 0; while(!stk.empty()) stk.pop(); for(root = 1; root <= n; ++ root){ if(!dfn[root]) tarjan(root, 0); } cout << "Case " << ++ T << ": "; if(cnt == 1){ n = cc[1].size(); cout << 2 << ' ' << n * (n - 1) / 2 << '\n'; }else { int ans1 = 0; ll ans2 = 1; for(int i = 1; i <= cnt; ++ i){ int ncut = 0; for(auto u : cc[i]){ ncut += cut[u]; } if(ncut == 1){ ++ ans1; ans2 *= (ll)cc[i].size() - 1; } } cout << ans1 << ' ' << ans2 << '\n'; } } return ; } signed main(){ ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr); int _ = 1; // cin >> _; while(_ --){ solve(); } return 0; }
割边
求割边 - 洛谷 P1656 炸铁路
//>>>Qiansui #include<bits/stdc++.h> #define ll long long #define ull unsigned long long #define mem(x,y) memset(x, y, sizeof(x)) #define debug(x) cout << #x << " = " << x << '\n' #define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << '\n' //#define int long long using namespace std; typedef pair<int, int> pii; typedef pair<ll, ll> pll; typedef pair<ull, ull> pull; typedef pair<double, double> pdd; /* */ const int maxm = 150 + 5, inf = 0x3f3f3f3f, mod = 998244353; int n, m, tot = 1; vector<pii> e[maxm]; vector<pii> ans; int dfn[maxm], low[maxm], idx = 0; void tarjan(int u, int x){ dfn[u] = low[u] = ++ idx; for(auto [v, id] : e[u]){ if(!dfn[v]){ tarjan(v, id); low[u] = min(low[u], low[v]); if(low[v] > dfn[u]){//顶点 v 没有别的方式抵达 u 的上方 ans.push_back({u, v}); } }else if(x != id)//防止同一条边回跳 low[u] = min(low[u], dfn[v]); } return ; } void solve(){ cin >> n >> m; for(int i = 0; i < m; ++ i){ int u, v; cin >> u >> v; e[u].push_back({v, ++ tot}); e[v].push_back({u, tot}); } for(int i = 1; i <= n; ++ i){ if(!dfn[i]) tarjan(i, 0); } sort(ans.begin(), ans.end()); for(auto [x, y] : ans){ cout << x << ' ' << y << '\n'; } return ; } signed main(){ ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr); int _ = 1; // cin >> _; while(_ --){ solve(); } return 0; }
eDCC缩点 找叶子节点个数 - 洛谷 P2860 [USACO06JAN] Redundant Paths G
题意
给你一个无向连通图,请你在此基础上计算所需加入的新的最少的边数,使得整个图边双连通
思路
就是删除原图中的所有割边
先对原图进行 eDCC 缩点成树,那么我们就可以便捷的获得所有割边连成的树
若这棵树的叶子节点个数为
即新添加
如何判断叶子节点?判断每一个双连通分量的出边是否仅有 1 条,仅有 1 条时即为叶子节点
具体缩点代码与强连通分量缩点类似
代码
//>>>Qiansui #include<bits/stdc++.h> #define ll long long #define ull unsigned long long #define mem(x,y) memset(x, y, sizeof(x)) #define debug(x) cout << #x << " = " << x << '\n' #define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << '\n' //#define int long long using namespace std; typedef pair<int, int> pii; typedef pair<ll, ll> pll; typedef pair<ull, ull> pull; typedef pair<double, double> pdd; /* */ const int maxm = 5e3 + 5, inf = 0x3f3f3f3f, mod = 998244353; int n, m; vector<pii> e[maxm]; int dfn[maxm], low[maxm], bel[maxm], idx = 0, cnt = 0; stack<int> stk; vector<vector<int>> cc; void tarjan(int u, int id){ dfn[u] = low[u] = ++ idx; stk.push(u); for(auto [v, id2] : e[u]){ if(!dfn[v]){ tarjan(v, id2); low[u] = min(low[u], low[v]); }else if(id != id2) low[u] = min(low[u], dfn[v]); } if(dfn[u] == low[u]){//边双缩图成树 ++ cnt; vector<int> t; int v; do{ v = stk.top(); bel[v] = cnt; stk.pop(); t.push_back(v); }while(v != u); cc.push_back(t); } return ; } void solve(){ cin >> n >> m; for(int i = 0; i < m; ++ i){ int u, v; cin >> u >> v; e[u].push_back({v, i}); e[v].push_back({u, i}); } tarjan(1, -1); int leaf = 0;//统计叶子节点个数 for(int i = 0; i < cnt; ++ i){ int cnte = 0;//与其连接的割边数 for(auto u : cc[i]){ for(auto [v, id] : e[u]){ cnte += (bel[u] != bel[v]); } } leaf += (cnte == 1);//仅有一条割边连接时即为叶子节点 } cout << (leaf + 1) / 2 << '\n'; return ; } signed main(){ ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr); int _ = 1; // cin >> _; while(_ --){ solve(); } return 0; }
- eDCC缩点 找叶子节点个数 - 百炼oj 3091:Road Construction
注意输入不包含“Sample Input 1”等
具体思想与洛谷 P2860 一样
Qiansui_code
本文来自博客园,作者:Qiansui,转载请注明原文链接:https://www.cnblogs.com/Qiansui/p/17672308.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】