「Day-1 提高笔记-割点(割边)」
割点(割边)
前置知识
首先是一些简单的基础。
- 连通分量:在无向图中其实就是相当于连通块。
- \(dfs\) 序:利用 \(dfs\) 在树上遍历的过程。
- 割点:去除这个点后这个图的连通块又增加了。
- 割边:去除这个边后这个图的连通块又增加了。
实现思路
如果我们在一个连通分量里面对任意一个点做 \(dfs\) 序,那么,会产生一个深度优先生成树 \(T\)。我们可以很显然的得到定理1.
定理1:如果 \(s\) 是 \(T\) 的根节点,且 \(s\) 有大于等于 \(2\) 个子节点,那么 \(s\) 为割点。
同时我们仔细想一想,如果一个一般的点 \(u\),在什么情况下满足是一个割点呢?对,是当下面的子节点没有能退回 \(u\) 的父节点的时候。我们可以得到定理2。
定理2:如果 \(T\) 的非根节点 \(u\),当且仅当 \(u\) 存在一个子节点 \(v\),\(v\) 及其后代没有退回边连 \(u\) 的祖先,此时 \(u\) 为割点。
代码思路
根据定理1和定理2,我们应该如何编程实现呢?
设对于一个节点 \(u\),其直接后代为 \(v\)。
- 对于我们的 \(dfs\) 序,用 \(num\) 数组来记录对每个点的访问顺序,\(num\) 随着 \(dfs\) 的加深不断增大。
- 对于我们的退回思路,定义 \(\text{low[v]}\) 表示 \(v\) 及其后代能回连到祖先的 \(num\) 值。如果 \(\text{low[v] >= num[u]}\),就说明 \(v\) 一定没有回连的边,那么 \(u\) 一定是割点。这就是 定理2。
有趣的割边
对于以上的思路:如果将 \(\text{low[v] >= num[u]}\) 改为 \(\text{low[v] > num[u]}\),这就是求割边的代码,为什么?原因在于如果 \(u\) 的后代最多只能回退到 \(v\),就说明了边 \((u,v)\) 就是一条割边。
代码实现
P3388 【模板】割点(割顶)
#include<iostream>
#include<vector>
using namespace std;
const int MAXN = 2 * 1e4 + 5;
int low[MAXN], num[MAXN];
int tot = 0, n, m, ans = 0;
bool iscut[MAXN];
vector<int> G[MAXN];
void dfs(int u, int fa) {
low[u] = num[u] = ++tot;
int child = 0;
for (int v : G[u]) {
if (!num[v]) {
child++;
dfs(v, u);
low[u] = min(low[u], low[v]);//用后代的值更新一下
if (low[v] >= num[u] && u != fa) iscut[u] = true;//定理2
}
else if (num[v] < num[u] && v != fa) {//处理回连边
low[u] = min(low[u], num[v]);
}
}
if (u == fa && child >= 2) iscut[u] = true;//定理1
}
int main() {
cin >> n >> m;
for (int i = 1, u, v; i <= m; i++) {
cin >> u >> v;
G[u].push_back(v);
G[v].push_back(u);
}
for (int i = 1; i <= n; i++) {
if (!num[i]) dfs(i, i);//题目中说图可能不联通
}
for (int i = 1; i <= n; i++) if (iscut[i]) ans++;
cout << ans << '\n';
for (int i = 1; i <= n; i++) if (iscut[i]) cout << i << ' ';
return 0;
}
本文来自一名初中牲,作者:To_Carpe_Diem