「Day-1 提高笔记-割点(割边)」

割点(割边)

前置知识

首先是一些简单的基础。

  1. 连通分量:在无向图中其实就是相当于连通块。
  2. \(dfs\) 序:利用 \(dfs\) 在树上遍历的过程。
  3. 割点:去除这个点后这个图的连通块又增加了。
  4. 割边:去除这个边后这个图的连通块又增加了。

实现思路

如果我们在一个连通分量里面对任意一个点做 \(dfs\) 序,那么,会产生一个深度优先生成树 \(T\)。我们可以很显然的得到定理1.

定理1:如果 \(s\)\(T\) 的根节点,且 \(s\) 有大于等于 \(2\) 个子节点,那么 \(s\) 为割点。

同时我们仔细想一想,如果一个一般的点 \(u\),在什么情况下满足是一个割点呢?对,是当下面的子节点没有能退回 \(u\) 的父节点的时候。我们可以得到定理2

定理2:如果 \(T\) 的非根节点 \(u\),当且仅当 \(u\) 存在一个子节点 \(v\)\(v\) 及其后代没有退回边连 \(u\) 的祖先,此时 \(u\) 为割点。

代码思路

根据定理1定理2,我们应该如何编程实现呢?

设对于一个节点 \(u\),其直接后代为 \(v\)

  1. 对于我们的 \(dfs\) 序,用 \(num\) 数组来记录对每个点的访问顺序,\(num\) 随着 \(dfs\) 的加深不断增大。
  2. 对于我们的退回思路,定义 \(\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;
}
posted @ 2024-10-09 00:07  To_Carpe_Diem  阅读(11)  评论(0编辑  收藏  举报