洛谷P8436 【模板】 边双连通分量 题解

P8436 【模板】 边双连通分量

前言

本文聊一聊边双连通分量是什么、如何求边双连通分量。

概念

割边(桥):在一个联通的无向图中,若删掉某条边,使得这个无向图不连通,那么这条边就称为“割边(桥)”,一个无向图中可能有不止一条割边。

双联通子图:在原图的某个子图 \(G'\) 中,不存在割边,那么 \(G'\) 就为双联通子图。

边双连通分量:原图中的极大双联通子图就为边双连通分量。形式化地,若某个双联通子图 \(G'\), 不存在其他的一个双联通子图 \(G''\),使得 \(G' \subsetneq G''\),那么 \(G'\) 就为边双连通分量,简称“e-DCC”。

找割边

找到边双连通分量的第一步是设法找到割边,颜值超级超级高的 tarjan 给出了很巧妙的求解方法。

不难发现,割边的两个端点,只有一条路径相连(这是定义嘛),也就是说,如果我们吧这个路径堵住,那么它们就不会联通。

代码该怎样写?

  • 引入一个时间戳,表示对应节点在遍历时是第几个被访问到的,每到达一个点先给这个点附上时间戳的值,随后时间戳 +1,等待下一次赋值。

  • 对于当前点 \(u\) ,令上一次遍历时到达到它的有向边为 \(e\),我们遍历其除了 \(e\) 的反向边以外的所有连边,如果某条边是割边,那么这条边遍历的点一定到不了之前遍历过的点。对于每个能到达的点,我们更新对应点的时间戳,使其时间戳尽量小(取最小值操作),如果某个点更新完毕后的时间戳不大于更新前的对应点的时间戳,说明这两个点之间的边就是割边。

代码如下

void tarjan(int u, int from)
{
    dfn[u] = low[u] = ++ timestamp; //timestamp为时间戳
    
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (!dfn[j])
        {
            tarjan(j, i);
            low[u] = min(low[u], low[j]);
            if (low[j] > dfn[u])
                is_bridge[i] = is_bridge[i ^ 1] = true; //is_bridge来维护i是否为桥
        }
        else if (i != (1 ^ from)) //不能走反向边
            low[u] = min(low[u], dfn[j]); //更新时间戳
    }
    return;
}

找边双连通分量

有上面的基础,这个就简单了。

如果经过一通更新发现它的时间戳没有变化,说明这个点已经是边界了。

维护一个栈来统计遍历的点数,到达边界后弹出直至弹到最初遍历节点,所弹的所有节点构成一个边双连通分量。

代码如下

void tarjan(int u, int from)
{
    dfn[u] = low[u] = ++ timestamp;
    stk[ ++ top] = u;
    
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (!dfn[j])
        {
            tarjan(j, i);
            low[u] = min(low[u], low[j]);
            if (low[j] > dfn[u])
                is_bridge[i] = is_bridge[i ^ 1] = true;
        }
        else if (i != (1 ^ from))
            low[u] = min(low[u], dfn[j]);
    }
    
    if (dfn[u] == low[u])
    {
        dcc_cnt ++ ; //dcc_cnt为边双连通分量编号
        int y;
        do
        {
            y = stk[top -- ];
            id[y] = dcc_cnt; //id为某点的所在边双连通分量编号
        } while (y != u);
    }
    
    return;
}

统计答案

这个问题,对于总的边双连通分量个数,答案就为 dcc_cnt 的值;而对于每一个边双连通分量中的点的个数,我们开个数组 tot 来维护,这个很简单;对于节点编号,我们之前的 id 数组起到了至关重要的作用,在此基础上再用一个 vector 来存答案即可。(详见代码)

Code

#include <iostream>
#include <cstring>
#include <vector>

using namespace std;

const int N = 500010, M = 2000010 * 2;

int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
int id[N], dcc_cnt;
bool is_bridge[M];
int tot[N];
vector <int> ans[N];

void add(int a, int b)
{
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx ++ ;
    return;
}

void tarjan(int u, int from)
{
    dfn[u] = low[u] = ++ timestamp;
    stk[ ++ top] = u;
    
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (!dfn[j])
        {
            tarjan(j, i);
            low[u] = min(low[u], low[j]);
            if (low[j] > dfn[u])
                is_bridge[i] = is_bridge[i ^ 1] = true;
        }
        else if (i != (1 ^ from))
            low[u] = min(low[u], dfn[j]);
    }
    
    if (dfn[u] == low[u])
    {
        dcc_cnt ++ ;
        int y;
        do
        {
            y = stk[top -- ];
            id[y] = dcc_cnt;
        } while (y != u);
    }
    
    return;
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> m;
    memset(h, -1, sizeof h);
    for (int i = 1; i <= m; i ++ )
    {
        int a, b;
        cin >> a >> b;
        add(a, b);
        add(b, a);
    }
    
    for (int i = 1; i <= n; i ++ )
        if (!dfn[i]) tarjan(i, -1);
    
    cout << dcc_cnt << endl;
    
    for (int i = 1; i <= n; i ++ )
    {
        tot[id[i]] ++ ;
        ans[id[i]].push_back(i);
    }
    //统计答案
    int p = 1;
    while (tot[p])
    {
        cout << tot[p] << ' ';
        for (int i = 0; i < ans[p].size(); i ++ )
            cout << ans[p][i] << ' ';
        p ++ ;
        cout << endl;
    }
    
    return 0;
}

后语

应用练习 P2860

posted @ 2022-07-17 16:15  LittleMoMol  阅读(286)  评论(0编辑  收藏  举报
*/