P8436 【模板】边双连通分量

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 5e5 + 10, inf = 0x3f3f3f3f;

// 边的结构体:v-目标节点,id-边的编号(用于区分重边)
struct node{
    int v, id;
};

vector<node> g[N];  // 邻接表存储图
int n, m;           // n: 节点数, m: 边数
int id;             // 边的编号计数器,用于给每条边一个唯一编号

// Tarjan算法核心数组
int dfn[N], low[N], cnt;  // dfn:时间戳,low:能回溯到的最早时间戳,cnt:时间戳计数器
int inq[N];                // 标记节点是否在栈中(这里其实用不到,但保留了)

// 栈相关
int s[N], top;             // 数组模拟栈,存储DFS路径上的节点

// 边双连通分量相关
int sccnt;                 // 边双连通分量计数器
vector<int> Scc[N];        // 存储每个边双连通分量包含的节点

/**
 * Tarjan算法求边双连通分量
 * @param x 当前访问的节点
 * @param bh 进入当前节点的边的编号(用于防止走反边,即父边)
 */
void tarjan(int x, int bh)
{
    // 初始化当前节点的时间戳和low值
    dfn[x] = low[x] = ++cnt;
    inq[x] = 1;          // 标记节点在栈中
    s[++top] = x;        // 节点入栈
    
    // 遍历当前节点的所有邻接边
    for(int i = 0; i < g[x].size(); i++)
    {
        int y = g[x][i].v;      // 邻接节点
        int Id = g[x][i].id;    // 当前边的编号
        
        // 如果当前边是进入x的边(父边),则跳过,防止走反边
        // 注意:这里处理了重边的情况,因为重边有不同的id
        if(bh == Id) continue;
        
        if(!dfn[y]){            // 如果y未被访问过(树边)
            tarjan(y, Id);       // 递归访问y
            // 回溯时用子节点的low值更新当前节点的low值
            low[x] = min(low[x], low[y]);
        }
        else {                   // y已被访问过(回边或横叉边)
            // 用y的时间戳更新当前节点的low值
            // 注意:这里不需要判断y是否在栈中,因为无向图没有横叉边的问题
            low[x] = min(low[x], dfn[y]);
        }
    }
    
    // 关键判断:如果low[x] == dfn[x],说明x是当前边双的根节点
    // 这意味着从x出发无法通过回边到达任何祖先节点
    // 此时栈中从x到栈顶的所有节点构成一个边双连通分量
    if(low[x] == dfn[x])
    {
        sccnt++;                 // 新的边双分量
        while(1)
        {
            int y = s[top--];    // 弹出栈顶节点
            Scc[sccnt].push_back(y);  // 将该节点加入当前边双
            if(y == x) break;    // 弹到x时停止(x是根)
        }
    }
}

/**
 * 调试函数:打印每个节点的dfn和low值
 */
void print()
{
    for(int i = 1; i <= n; i++)
        cout << dfn[i] << " " << low[i] << " " << i << endl;
}

int main()
{
    cin >> n >> m;
    id = 0;  // 边编号从0开始
    
    // 读入所有边
    for(int i = 1; i <= m; i++)
    {
        int x, y; 
        cin >> x >> y;
        if(x == y){
            // 自环处理:只加一条边,但用不同的id
            // 自环不会影响边双的连通性,但需要存储
            g[x].push_back({y, id});
            id++;
        }
        else{
            // 无向图:加双向边,使用相同的id表示这是同一条边
            g[x].push_back({y, id});
            g[y].push_back({x, id});
            id++;
        }
    }
    
    // 对每个未被访问的节点执行Tarjan算法
    // 图可能不连通,所以需要遍历所有节点
    for(int i = 1; i <= n; i++)
        if(!dfn[i])
            tarjan(i, -1);  // -1表示没有进入边(根节点)
    
    // 输出结果
    cout << sccnt << endl;  // 先输出边双连通分量的个数
    for(int i = 1; i <= sccnt; i++)
    {
        cout << Scc[i].size() << " ";  // 输出当前分量的节点数
        for(int j = 0; j < Scc[i].size(); j++)
            cout << Scc[i][j] << " ";  // 输出当前分量的所有节点
        cout << endl;
    }
    
    // print();  // 调试用,可以取消注释查看dfn和low值
    
    return 0;
}

 

posted @ 2026-03-28 20:26  CRt0729  阅读(20)  评论(0)    收藏  举报