双连通分量
边/点双连通: 如果无论删去哪个边/点 (只能删去一个), 两点仍然连通.
边/点双连通分量: 任意两个点都满足条件的子图.
其中边双连通具有传递性. =>
AB, BC, AC 边双连通; AB, BC 点双连通
由此可见: 点双连通两点一定在一个环上, 而边双连通的两点可能是在一个扭起来的环上(如图, 点多次使用, 但边只用一次的环, 可以拆解为多个环) (见题 link, 只需边双连通就能满足 Lun 边的要求.)
也就是说, 边双可能含有割点. 不同点双可能公用某点 (割点), 边双无公用.
1. 求边双连通分量
-
Tarjan 算法 1
-
先删掉所有桥 => Tarjan( 系列2) 在无向图中找割点/桥
桥 (-> 删除一条边使两点不连通 ), 故桥两边的点一定不满住边双
-
这时的所有连通分量都是边双连通分量.
//消耗内存较大, 内存较小时可以用差分法求桥, 并查集优化 // ac [https://www.luogu.com.cn/problem/P8436] int dfsn[Maxn], low[Maxn], vis[Maxn], cnt = 0; vector<vector<int>>ans; vector<int>eg[Maxn]; set<pair<int, int>>is_bridge; void tarjan(int tis, int fa) { low[tis] = dfsn[tis] = ++cnt; for (auto c : eg[tis]) { if (!dfsn[c]) { tarjan(c, tis); low[tis] = min(low[c], low[tis]); if (low[c] > dfsn[tis]) { //(tis,c) is bridge is_bridge.emplace(tis, c); is_bridge.emplace(c, tis); } } else if (fa != c) { low[tis] = min(low[tis], dfsn[c]);//反向边仅在此出现 if (is_bridge.count({c, tis})) { // 处理重边 is_bridge.erase(is_bridge.find({c, tis})); is_bridge.erase(is_bridge.find({tis, c})); } } } } void dfs(int tis, int num) { vis[tis] = 1; ans[num].emplace_back(tis); for (auto &c : eg[tis]) { if (vis[c])continue; if (is_bridge.count({c, tis}))continue; dfs(c, num); } } signed main() { ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); int n, m, v, u; cin >> n >> m; for (int i = 1; i <= m; ++i) { cin >> v >> u; if (u == v)continue; // 去掉自环 eg[u].emplace_back(v); eg[v].emplace_back(u); } for (int i = 1; i <= n; ++i) { if (dfsn[i])continue; tarjan(i, i); } debug(is_bridge) int num = 0; for (int i = 1; i <= n; ++i) { if (vis[i])continue; ans.emplace_back(vector<int>()); dfs(i, ++num - 1); } } /* ================================== */ //前向星版 const int maxn = 5e5 + 10, maxm = 4e6 + 10; int dfsn[maxn], low[maxn], head[maxn], vis[maxn]; struct edge { int nxt, to; } e[maxm]; bool b[maxm]; vector< vector<int > > Ans; int cnt = 1, id = 0, ans = 0; void add(int u, int v) { e[++cnt].nxt = head[u]; e[cnt].to = v; head[u] = cnt; } void tarjan(int now, int in_edge) { dfsn[now] = low[now] = ++id; for (int i = head[now]; i > 0; i = e[i].nxt) { int to = e[i].to; if (!dfsn[to]) { tarjan(to, i); if (dfsn[now] < low[ to]) b[i] = b[i ^ 1] = 1; low[now] = min(low[now], low[to]); } else if (i != (1 ^ in_edge)) low[now] = min(low[now], dfsn[to]); } } void dfs(int now, int num) { vis[now] = 1; Ans[num].push_back(now); for (int i = head[now]; i > 0; i = e[i].nxt) { int to = e[i].to; if (vis[to] || b[i]) continue; dfs(to, num); } } //输入 for (int i = 1; i <= m; i++) { int x, y; cin >> x >> y; if (x == y) continue; add(x, y); add(y, x); }
-
2.点双
两个性质:
- 两个点双最多只有一个公共点,且一定是割点。
- 对于一个点双,它在 DFS 搜索树中 dfn 值最小的点一定是割点或者树根 (原理参照 Tarjan 求割点 )。
故: 一个点是割点时, 它一定是点双分量的根. 如果再往上包含它的父亲, 那它就成为分量内的割点了. 操作类似求有向图的强连通分量, 用一个栈去存点. =>Tarjan( 系列1) 求强连通子图
// AC [https://www.luogu.com.cn/problem/P8435]
const int Maxn = 5e5 + 10;
int n, m, v, u;
vector<int>eg[Maxn];
vector<vector<int>>dcc;
int dfsn[Maxn], low[Maxn], st[Maxn];
int dfsn_cnt = 0, dcc_cnt = 0, top = 0;
void tarjan(int tis, bool rot = true) {
low[tis] = dfsn[tis] = ++dfsn_cnt;
st[top++] = tis;
if (rot && eg[tis].empty()) { // 根为孤立点
dcc.emplace_back(vector<int>());
dcc[dcc_cnt++].emplace_back(tis);
}
for (auto c : eg[tis]) {
if (!dfsn[c]) {
tarjan(c, 0);
low[tis] = min(low[c], low[tis]);
if (low[c] >= dfsn[tis]) { // tis 为割开 tis->c 的割点
dcc.emplace_back(vector<int>());
while (top && st[top - 1] != c) {
dcc[dcc_cnt].emplace_back(st[--top]);
}
dcc[dcc_cnt].emplace_back(st[--top]);
dcc[dcc_cnt].emplace_back(tis); // 割点可能与其他分量公用, 不从栈中移除
dcc_cnt++;
}
}
else
low[tis] = min(low[tis], dfsn[c]);
}
}
signed main()
{
ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= m; ++i) {
cin >> v >> u;
if (u == v)continue;
eg[u].emplace_back(v);
eg[v].emplace_back(u);
}
for (int i = 1; i <= n; ++i) {
if (!dfsn[i]) {
tarjan(i);
}
}
}