随笔 - 164  文章 - 0  评论 - 4  阅读 - 9882

割点、割边、双连通分量

割点、割边

割点 和 割边 就是特定的点和边,满足在图中删去其后,会使得图的连通分支数增加

例如下图中点 0, 1, 5, 7 就是割点(图中加粗),边 12、05、78、79 就是割边
image

求割点、割边

求割点 - 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,那么边 (u,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 不是割点时,答案即为 2n2 ,因为只有节点 i 与其余点能够构成合法点对
当图中节点 i 是割点时,删除其所有边会将图划分为 k 个连通块和 i 本身,那么每一个连通块均与剩余的点产生贡献。设 tarjan 求割点时节点 i 及其子树包含的点数为 cut(与下方代码中的 cut 表示意义不同),sz[vj] 表示节点 j 及其子树的节点数,则 ans[i]=sz[v1]×(nsz[v1])+sz[v2]×(nsz[v2])+...+sz[vk]×(nsz[vk])+n1+cut×(ncut)

具体细节见代码

代码

//>>>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 缩点得到每个整个图的所有点双分量
如果整个图是点联通的,那么 ans=(n2)
否则,我们需要在每个叶子节点处设置一个救援点,仅含有一个割点的点双分量就是叶子节点,而救援点的安放就是当前点双分量中处割点外的任意一点即可

代码

//>>>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 缩点成树,那么我们就可以便捷的获得所有割边连成的树

若这棵树的叶子节点个数为 n,则答案为 (n+1)/2
即新添加 (n+1)/2 条边即可达成目标,因为这样的新加边能够最少的完全覆盖所有的树边

如何判断叶子节点?判断每一个双连通分量的出边是否仅有 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;
}

posted on   Qiansui  阅读(49)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

点击右上角即可分享
微信分享提示