强连通分+双连通分量+圆方树
强连通分量
求所有割边(桥)
割边定义:对于一个无向图,如果删掉一条边后图中的连通分量数增加了,这条边就是割边。
模板:https://www.luogu.com.cn/problem/P1656
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
using PII = pair<int, int>;
const int MAXN = 150 + 3, MAXM = 5000 + 3;
struct Edge {
int nxt, id;
};
int n, m, ANS_len = 0;
int dep[MAXN], low[MAXN];
PII _eg[MAXM], ANS[MAXM];
vector<Edge> eg[MAXN];
int dfs(int x, int dad, int e) {
if(dep[x] > 0) return dep[x]; // 这里也可以返回 low
dep[x] = low[x] = dep[dad] + 1; // 这里也可以 depth 进行累加
for(Edge E : eg[x]){
if(E.id != e) low[x] = min(low[x], dfs(E.nxt, x, E.id));
}
if(e > 0 && low[x] == dep[x]) ANS[++ANS_len] = _eg[e];
return low[x];
}
int main() {
cin >> n >> m;
for (int i = 1, U, V; i <= m; i++) {
cin >> U >> V;
if (U > V) swap(U, V); // U <= V
_eg[i] = {U, V}, eg[U].push_back({V, i}), eg[V].push_back({U, i});
}
dfs(1, 0, 0);
sort(ANS + 1, ANS + 1 + ANS_len); // pair 自带比较规则:先比较 first 再比较 second
for (int i = 1; i <= ANS_len; i++) {
cout << ANS[i].first << " " << ANS[i].second << "\n";
}
return 0;
}
求所有割点
割点定义:对于一个无向图,如果把一个点删除后这个图的极大连通分量数增加了,那么这个点就是这个图的割点(又称割顶)。
模板:https://www.luogu.com.cn/problem/P3388
参考题解:https://www.luogu.com.cn/article/pqth396e
无向图 DFS 树的性质(非常重要):
- 祖先后代性:任意非树边两端具有祖先后代关系。
- 子树独立性:结点的每个儿子的子树之间没有边(和上一条性质等价)。
- 时间戳区间性:子树时间戳为一段区间。
- 时间戳单调性:结点的时间戳小于其子树内结点的时间戳。
设 \(T(x)\) 表示树上 \(x\) 子树(含 \(x\)),设 \(T'(x)\) 表示除 \(T(x)\) 以外的所有节点。
因为子树独立性,对于 \(T(x)\) 如果删除 \(x\) 可能会分裂成多个联通块。但是 \(T'(x)\) 始终是联通的一个联通块。
所以 \(T(y)\) 不会被孤立的唯一希望就是于 \(T'(x)\) 有连边。
根据割点定义,我们可以转化为以下问题:若 \(x\) 是割点,等价于存在 \(u \in T(x)\) 不经过 \(x\) 可以一步到达 \(v \in T'(x)\)
若 \((u,v)\) 是树边,则 \(u\) 必须等于 \(x\),矛盾。所以 \((u,v)\) 为非树边。
我们只需要判断 \(x\) 是否存在一个儿子 \(y\) ,满足存在 \(u\in T(y)\) 可以可以通过一步到达 \(v \in T'(x)\)。
设 \(f_y\) 表示 \(u \in T(y)\) 可以通过返祖边到达的最大时间戳。
如果 \(x\) 是割点,也就是:存在一个 \(x\) 的儿子 \(y\) 满足 \(f_y \ge d_x\)。
但是实现的时候,对于根节点,需要特别处理,看代码。
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
const int MAXN = 2e4 + 3, MAXM = 1e5 + 3;
struct Edge {
int nxt, id;
};
int n, m, ANS_len = 0;
int dep[MAXN], low[MAXN], cnt[MAXN], ANS[MAXN], nd_vis[MAXN];
vector<Edge> eg[MAXN];
int dfs(int x, int dad) {
if(dep[x] > 0) return dep[x];
dep[x] = low[x] = dep[dad] + 1; // 这里也可以 depth 进行累加
for(Edge E : eg[x]){
low[x] = min(low[x], dfs(E.nxt, x));
}
if(cnt[x] >= 1 + (dad == 0)){
ANS[++ANS_len] = x;
}
cnt[dad] += (dep[dad] <= low[x]);
return low[x];
}
int main() {
cin >> n >> m;
for (int i = 1, U, V; i <= m; i++) {
cin >> U >> V, eg[U].push_back({V, i}), eg[V].push_back({U, i});
}
for(int i = 1; i <= n; i++) dfs(i, 0);
sort(ANS + 1, ANS + 1 + ANS_len);
cout << ANS_len << "\n";
for (int i = 1; i <= ANS_len; i++) {
cout << ANS[i] << " ";
}
return 0;
}
缩点
若两点之间可以互相到达,则两点是 强连通
若一个有向图任意两点之间强连通,则该有向图是 强联通图。
有向图的一个极大强连通子图,为 强连通分量 (Strongly Connected Component,SCC)。
注意到每个 强连通分量 都没有交。
缩点就是缩每个强联通分量。
模板:https://www.luogu.com.cn/problem/P3387
参考题解:https://www.cnblogs.com/alex-wei/p/basic_graph_theory.html 的 4.3 部分
注意有向图的 DFS 序有额外的两种边:
- 从祖先指向后代的非树边,称为 前向边。
- 从后代指向祖先的非树边,称为 返祖边。
- 两端无祖先后代关系的非树边,称为 横叉边。
考虑在最浅的结点处求出包含它的 SCC。称一个点是关键点,当且仅当它是某个 SCC 的最浅结点。
考虑何时 \(x\) 和 \(fa\) 两点强连通(看上面的图)
- \(x\) 到 \(u1\) 然后再走返祖边到达 \(fa\)
- \(x\) 走横插边到 \(v2\),且 \(v2\) 可以到达 \(fa\)
设 \(g_x\) 表示 \(x\) 可以 通过返祖边 和 通过横插边 到达的最小时间戳。
结论:\(x\) 是关键点,当且仅当 \(g_x \ge d_x\)。证明看 https://www.cnblogs.com/alex-wei/p/basic_graph_theory.html 的 4.3 部分。
考虑如何求 \(g\)。
当找到了一个强联通分量时,则这个强联通分量不会对外贡献。
然后就是代码那种实现了。
#include <algorithm>
#include <iostream>
#include <vector>
#include <cmath>
using namespace std;
using LL = long long;
const int MAXN = 1e4 + 3, MAXM = 1e5 + 3;
int n, m, depth = 0, ANS = 0;
int s[MAXN], l = 0; // dfs序
int a[MAXN], dp[MAXN];
int dep[MAXN], low[MAXN], root[MAXN];
vector<int> eg[MAXN];
int dfs(int x){
if(dep[x]) return (root[x] > 0 ? n : low[x]); // can dep too
depth++, dep[x] = low[x] = depth;
s[++l] = x;
for(int E : eg[x]){
low[x] = min(low[x], dfs(E));
}
if(low[x] == dep[x]){
for( ; l > 0 && s[l] != x; l--){
for(int E : eg[s[l]]) eg[x].push_back(E);
root[s[l]] = x, a[x] += a[s[l]];
}
root[x] = x, l--;
}
return low[x];
}
int a_dfs(int x){
if(dp[x] == -1){
dp[x] = 0;
for(int E : eg[x]){
dp[x] = max(dp[x], a_dfs(root[E]));
}
dp[x] += a[x];
}
return dp[x];
}
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1, U, V; i <= m; i++){
cin >> U >> V;
eg[U].push_back(V);
}
for(int i = 1; i <= n; i++) dfs(i);
fill(dp + 1, dp + 1 + n, -1);
for(int i = 1; i <= n; i++){
ANS = max(ANS, a_dfs(root[i]));
}
cout << ANS;
return 0;
}
双连通分量
定义
在一张连通的无向图中,对于两个点 \(u\) 和 \(v\),如果无论删去哪条边(只能删去一条)都不能使它们不连通,我们就说 \(u\) 和 \(v\) 边双连通。
在一张连通的无向图中,对于两个点 \(u\) 和 \(v\),如果无论删去哪个点(只能删去一个)都不能使它们不连通,我们就说 \(u\) 和 \(v\) 点双连通。
- 点双连通图:不存在割点的无向连通图称为 点双连通图。根据割点的定义,孤立点和孤立边均为点双连通图。
- 边双连通图:不存在割边的无向连通图称为 边双连通图。根据割边的定义,孤立点是边双连通图,但孤立边不是。
- 点双连通分量:一张图的极大点双连通子图称为 点双连通分量(V-BCC),简称 点双。
- 边双连通分量:一张图的极大边双连通子图称为 边双连通分量(E-BCC),简称 边双。
边双连通分量
一个割边可以将图分为 \(2\) 个连通块,\(s\) 割边可以将图分为 \(s+1\) 个连通块。断开所有割边,我们就得到了 \(s+1\) 个边双连通分量。
性质:
- 边双连通的传递性
- 若 \(a\) 于 \(b\) 边双连通,\(b\) 于 \(c\) 边双连通,则有 \(a\) 与 \(c\) 边双连通。
模板题:https://www.luogu.com.cn/problem/P8436
点击查看代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int MAXN = 5e5 + 3, MAXM = 2e6 + 3;
struct Edge{
int to, id;
};
int n, m, k = 0;
vector<int> ans[MAXN];
vector<Edge> eg[MAXN];
int depth = 0, dep[MAXN], low[MAXN];
vector<int> stk;
int dfs(int x, int fe){
if(dep[x]) return dep[x];
dep[x] = low[x] = ++depth;
stk.push_back(x);
for(Edge e : eg[x]){
if(e.id == fe) continue;
low[x] = min(low[x], dfs(e.to, e.id));
}
if(dep[x] == low[x]){
k++;
while(stk.size() > 0 && stk.back() != x){
ans[k].push_back(stk.back()), stk.pop_back();
}
ans[k].push_back(stk.back()), stk.pop_back();
}
return low[x];
}
int main(){
cin >> n >> m;
for(int i = 1, U, V; i <= m; i++){
cin >> U >> V;
eg[U].push_back({V, i});
eg[V].push_back({U, i});
}
for(int i = 1; i <= n; i++) dfs(i, 0);
cout << k << "\n";
for(int i = 1; i <= k; i++){
cout << ans[i].size() << " ";
for(int x : ans[i]) cout << x << " ";
cout << "\n";
}
return 0;
}
点双连通分量
性质:
- 若两点双有交,那么交点一定是割点。
- 一个点是割点当且仅当它属于超过一个点双。
模板题:https://www.luogu.com.cn/problem/P8435
实现:
点击查看代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int MAXN = 5e5 + 3, MAXM = 2e6 + 3;
struct Edge{
int to, id;
};
int n, m, k = 0;
int vis[MAXN], check[MAXN];
vector<int> ans[MAXM];
vector<Edge> eg[MAXN];
int depth = 0, dep[MAXN], low[MAXN], cnt[MAXN];
vector<int> stk;
void dfs(int x, int fx){
dep[x] = low[x] = ++depth;
stk.push_back(x);
bool ooo = 0;
for(Edge e : eg[x]){
if(dep[e.to]){
low[x] = min(low[x], dep[e.to]);
continue;
}
ooo = 1, dfs(e.to, x), low[x] = min(low[x], low[e.to]);
if(dep[x] <= low[e.to]){
k++;
while(stk.size() > 0){
ans[k].push_back(stk.back()), stk.pop_back();
if(ans[k].back() == e.to) break;
}
ans[k].push_back(x);
}
}
if(fx == 0 && ooo == 0) ans[++k].push_back(x);
}
int main(){
cin >> n >> m;
for(int i = 1, U, V; i <= m; i++){
cin >> U >> V;
if(U == V) continue;
eg[U].push_back({V, i});
eg[V].push_back({U, i});
}
for(int i = 1; i <= n; i++){
if(dep[i] == 0) stk.clear(), dfs(i, 0);
}
cout << k << "\n";
for(int i = 1; i <= k; i++){
cout << ans[i].size() << " ";
for(int x : ans[i]) cout << x << " ";
cout << "\n";
}
return 0;
}
圆方树
点双连通图:有两种定义,两种是等价的
- 图中任意两不同点之间都有至少两条点不重复的路径。
- 图中不存在割点
点双连通分量 则是一个 极大点双连通子图。
点双联通图有的性质:
- P4630 [APIO2018] 铁人两项: 对于一个点双中的两点,它们之间简单路径的并集,恰好完全等于这个点双。即两点之间,对于任意点双内的点 \(w\),一定有一条路径经过它。(证明可以看 oi-Wiki)
- 它告诉了我们:考虑两圆点在圆方树上的路径,与路径上经过的方点相邻的圆点的集合,就等于原图中两点简单路径上的点集。
应为割点数量小于等于 \(n\),所以圆方树节点个数小于等于 \(2n\),所以所有数组都要开两倍。
oi-wiki 上讲的圆方树:https://oi-wiki.org/graph/block-forest/
点击查看 铁人两项 代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int MAXN = 2e5 + 3;
int n, m, k = 0, w[MAXN];
vector<int> _eg[MAXN], eg[MAXN];
int depth = 0, dfn[MAXN], low[MAXN];
vector<int> stk;
void dfs(int x){
low[x] = dfn[x] = ++depth, stk.push_back(x);
for(int nxt : _eg[x]){
if(!dfn[nxt]){
dfs(nxt);
low[x] = min(low[x], low[nxt]); // 细节,挂了一次
if(low[nxt] == dfn[x]){
k++;
int back = 0;
while(back != nxt){ // 细节,挂了一次
back = stk.back();
w[k]++, eg[k].push_back(stk.back()), eg[stk.back()].push_back(k);
stk.pop_back();
}
w[k]++, eg[k].push_back(x), eg[x].push_back(k);
}
}else low[x] = min(low[x], dfn[nxt]);
}
}
void init(){
cin >> n >> m, k = n;
for(int i = 1, U, V; i <= m; i++){
cin >> U >> V, _eg[U].push_back(V), _eg[V].push_back(U);
}
for(int i = 1; i <= n; i++){
w[i] = -1;
if(!dfn[i]) stk.clear(), dfs(i);
}
swap(n, k);
}
int vis[MAXN], sz[MAXN];
LL ans = 0;
vector<int> vt;
void dfs2(int x){
vt.push_back(x);
int op = (x <= k);
sz[x] = op, vis[x] = 1;
for(int nxt : eg[x]){
if(!vis[nxt]){
dfs2(nxt);
ans += 2ll * w[x] * sz[x] * sz[nxt];
sz[x] += sz[nxt];
}
}
}
int main(){
ios::sync_with_stdio(0), cin.tie(0);
init();
for(int i = 1; i <= n; i++){
if(!vis[i]){
vt.clear(), dfs2(i);
for(int x : vt){
ans += 2ll * w[x] * sz[x] * (sz[i] - sz[x]); // 细节,挂了一次(不能将 sz[i] 改为 k)
}
}
}
cout << ans;
return 0;
}
练习:https://www.luogu.com.cn/problem/CF487E