割点、割边、双连通分量

割点、割边

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

例如下图中点 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 不是割点时,答案即为 $2n - 2 $ ,因为只有节点 i 与其余点能够构成合法点对
当图中节点 i 是割点时,删除其所有边会将图划分为 k 个连通块和 i 本身,那么每一个连通块均与剩余的点产生贡献。设 tarjan 求割点时节点 i 及其子树包含的点数为 cut(与下方代码中的 cut 表示意义不同),$sz[v_j] $ 表示节点 j 及其子树的节点数,则 $ans[i] = sz[v_1] \times (n - sz[v_1]) + sz[v_2] \times (n - sz[v_2]) + ... + sz[v_k] \times (n - sz[v_k]) + n - 1 + cut \times (n - 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 缩点得到每个整个图的所有点双分量
如果整个图是点联通的,那么 $ans = {n \choose 2} $
否则,我们需要在每个叶子节点处设置一个救援点,仅含有一个割点的点双分量就是叶子节点,而救援点的安放就是当前点双分量中处割点外的任意一点即可

代码

//>>>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 2023-09-01 16:36  Qiansui  阅读(27)  评论(0编辑  收藏  举报