洛谷 - P3388 【模板】割点(割顶)

模板题

// Problem: P3388 【模板】割点(割顶)
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3388
// Memory Limit: 125 MB
// Time Limit: 1000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include <cstdio>   // 用于输入输出函数,如scanf、printf
#include <cstring>  // 用于内存操作函数,如memset
#include <algorithm> // 用于算法函数,如min
using namespace std;  // 引入标准命名空间

// 定义常量:N为最大顶点数,M为最大边数(因为是无向图,每条边存储两次)
const int N = 2e4 + 10, M = 2 * (1e5 + 10);

int n, m;  // n表示顶点数,m表示边数

// 邻接表存储图:
// e[]存储边的终点,ne[]存储下一条边的索引,h[]存储头节点的边索引,idx为边的计数器
int e[M], ne[M], h[N], idx;

// dfn[]存储节点的发现时间(深度优先搜索时的访问顺序)
// low[]存储节点能访问到的最早发现时间的节点
// timestamp为时间戳计数器,记录访问顺序
int dfn[N], low[N], timestamp;

bool cut[N];  // 标记数组,cut[u]为true表示u是割点

// 向邻接表中添加一条从a到b的边
// 无向图需要添加两次(a->b和b->a)
void add(int a, int b) {
    e[idx] = b;      // 存储当前边的终点
    ne[idx] = h[a];  // 将当前边的下一条边设置为a原来的第一条边
    h[a] = idx++;    // 更新a的第一条边为当前边,边计数器加1
}

// Tarjan算法核心函数,用于寻找割点
// u表示当前节点,fa表示当前节点的父节点(-1表示没有父节点,即根节点)
void tarjan(int u, int fa) {
    dfn[u] = low[u] = ++timestamp;  // 初始化发现时间和最早访问时间为当前时间戳,时间戳加1
    int son = 0;  // 记录当前节点的子树数量(在深度优先搜索树中)
    
    // 遍历当前节点的所有邻接边
    // i初始为h[u](第一条边),每次更新为ne[i](下一条边),直到i=-1(没有更多边)
    for (int i = h[u]; ~i; i = ne[i]) {
        int v = e[i];  // 获取当前边的终点v
        
        if (v == fa) continue;  // 如果v是父节点,则跳过(避免回头访问)
        
        if (!dfn[v]) {  // 如果v还没有被访问过(未发现)
            tarjan(v, u);  // 递归访问v,当前节点u作为v的父节点
            
            // 回溯时,更新当前节点的low值为自身low值和子节点v的low值中的较小者
            low[u] = min(low[u], low[v]);
            
            // 若子节点v的最早访问时间大于等于当前节点u的发现时间,且u不是根节点
            // 说明v无法绕过u访问到更早的节点,因此u是割点
            if (low[v] >= dfn[u] && fa != -1) {
                cut[u] = true;
            }
            
            son++;  // 子树数量加1
        } else {
            // 如果v已经被访问过(已发现),说明遇到了回边
            // 更新当前节点的low值为自身low值和v的发现时间中的较小者
            low[u] = min(low[u], dfn[v]);
        }
    }
    
    // 特殊情况:如果是根节点(fa=-1)且子树数量大于1
    // 说明删除根节点会导致图分裂成多个部分,因此根节点是割点
    if (fa == -1 && son > 1) {
        cut[u] = true;
    }
}

int main() {
    scanf("%d%d", &n, &m);  // 读取顶点数和边数
    
    memset(h, -1, sizeof h);  // 初始化邻接表头节点为-1,表示没有边
    
    // 读取m条边,并构建邻接表(无向图,每条边添加两次)
    for (int i = 1, a, b; i <= m; i++) {
        scanf("%d%d", &a, &b);  // 读取一条边的两个端点
        add(a, b);  // 添加a到b的边
        add(b, a);  // 添加b到a的边(无向图)
    }
    
    // 遍历所有节点,对未访问过的节点(可能存在多个连通分量)执行Tarjan算法
    for (int i = 1; i <= n; i++) {
        if (!dfn[i]) {  // 如果节点i未被访问过
            tarjan(i, -1);  // 以i为根节点开始遍历,父节点设为-1
        }
    }
    
    int ans = 0;  // 统计割点的数量
    for (int i = 1; i <= n; i++) {
        if (cut[i]) {  // 如果是割点
            ans++;  // 数量加1
        }
    }
    
    printf("%d\n", ans);  // 输出割点的数量
    
    // 输出所有割点(按节点编号从小到大)
    for (int i = 1; i <= n; i++) {
        if (cut[i]) {  // 如果是割点
            printf("%d ", i);  // 输出节点编号
        }
    }
    puts("");  // 输出换行,结束程序
    
    return 0;
}

posted @ 2025-08-20 16:17  九三青梧  阅读(14)  评论(0)    收藏  举报