跳转至

边双连通分量

边双连通分量(Edge-Biconnected Component)是图论中的一个概念,它指的是在一个无向图中,极大的一组顶点和边,使得任意两个顶点间都有至少两条不相交的边连接(即即使删除图中的一条边,这些顶点仍然是连通的)。换句话说,边双连通分量中的任意两个顶点间都是边双连通的。

例题 #1

对于一个 \(n\) 个节点 \(m\) 条无向边的图,请输出其边双连通分量的个数,并且输出每个边双连通分量。

提示&代码

void tarjan(int u,int eid) {//当前节点,入边编号
    dfn[u]=low[u]=++indx;
    stk[++tp]=u;
    for(int i=head[u]; ~i; i=edge[i].nxt) {
        int v=edge[i].to;
        if(dfn[v]==0) {
            tarjan(v,i);
            low[u]=min(low[u],low[v]);
        }
        if(i!=(eid^1)) low[u]=min(low[u],dfn[v]);
    }
    if(dfn[u]==low[u]) {
        edcc++;
        int x;
        do {
            x=stk[tp--];
            belong[x]=edcc;
        } while(x!=u);
    }
}

完整代码

/*////////ACACACACACACAC///////////
       . Code  by  Ntsc .
       . Earn knowledge .
/*////////ACACACACACACAC///////////

#include<bits/stdc++.h>
#define int long long
#define db double
#define rtn return
using namespace std;

const int N=5e5+5;
const int M=5e6;
const int Mod=1e5;
const int INF=1e5;

int n,m,p,T;



int dfn[N],low[N],indx;//时间戳,追溯值,给时间戳编号的计数器 
int stk[N],tp;//栈,指针 
bool instk[N]; //是否在栈中
int belong[N],siz[N],edcc; //记录每个点在那个边双连通分量里, 每个点所在的边双连通分量的大小,边双连通分量的数量 
vector<int> ans[N];

int vis[N];

struct edge{
    int to,nxt;
}e[M];
int h[N],idx=-1;
void add(int a,int b){
    e[++idx]={b,h[a]};
    h[a]=idx;
    e[++idx]={a,h[b]};
    h[b]=idx;
}

void tarjan(int u,int eid) {//当前节点,入边编号
    vis[u]=1;
    dfn[u]=low[u]=++indx;
    stk[++tp]=u;
    for(int i=h[u]; ~i; i=e[i].nxt) {
        int v=e[i].to;
        if(dfn[v]==0) {
            tarjan(v,i);
            low[u]=min(low[u],low[v]);
        }
        if(i!=(eid^1)) low[u]=min(low[u],dfn[v]);
    }
    if(dfn[u]==low[u]) {
        edcc++;
        int x;
        do {
            x=stk[tp--];
            belong[x]=edcc;
        } while(x!=u);
    }
}
signed main(){
    int m,n;
    cin>>n>>m;

    for(int i=1;i<=N;i++)e[i].nxt=-1,h[i]=-1;
    for(int i=1;i<=m;i++){
        int a,b;
        cin>>a>>b;
        add(a,b);
    }
    for(int i=1;i<=n;i++)if(!vis[i])tarjan(i,0);//图可能不连通!切勿重复访问!
    for(int i=1;i<=n;i++)ans[belong[i]].push_back(i);

    cout<<edcc<<endl; 
    for(int i=1;i<=edcc;i++){
        cout<<ans[i].size()<<' ';
        for(auto v:ans[i])cout<<v<<' ';
        cout<<endl;

    return 0;
}

例题 #2「EVOI-RD2」旅行家

题目描述

小 A 是一个热衷于旅行的旅行家。有一天,他来到了一个城市,这个城市由 \(n\) 个景点与 \(m\) 条连接这些景点的道路组成。每个景点有一个**美观度** \(a_i\)

定义一条**旅游路径**为两个景点之间的一条非严格简单路径,也就是点可以重复经过,而边不可以。

接下来有 \(q\) 个旅游季,每个旅游季中,小 A 将指定两个顶点 \(x\)\(y\),然后他将走遍 \(x\)\(y\) 的**所有旅游路径**。

所有旅游季结束后,小 A 会统计他所经过的所有景点的美观度之和(重复经过一个景点只统计一次美观度)。他希望你告诉他这个美观度之和。

输入格式

第一行两个正整数 \(n,m\)

第二行 \(n\) 个正整数 \(a_i\),代表顶点 \(i\) 的权值。

接下来 \(m\) 行,每行 \(2\) 个正整数,表示一条无向边的两个端点。

然后一个正整数 \(q\),代表有 \(q\) 个旅游季。

接下来 \(q\) 行,每行 \(2\) 个整数 \(x,y\),表示指定的起点和终点。

格式

一行,一个整数表示最终的美观度总和。

对于 \(100\%\) 的数据,保证 \(3 \leq n \leq 5 \times 10^5\)\(m \leq 2 \times 10^6\)\(q=10^6\)\(1 \leq a_i \leq 100\),且该图联通,没有重边和自环。


我们发现,x 到 y 的所有旅游路径可能经过的点就是edcc缩点后x,y路径上的所有点集。那么我们问题就变成了缩点后树上路径加,这个用树上差分简单解决。

本题存在重边。每个 dfs 都需要使用 vis 判重边,否则可以被卡到 \((\frac{M}{N})^N\)。可参考下图自行模拟。

![501f1e8c53b99c1e17bdab809a3b0c67.png](边双连通分量/501f1e8c53b99c1e17bdab809a3b0c67.png)
/*                                                                                
                      Keyblinds Guide
                    ###################
      @Ntsc 2024

      - Ctrl+Alt+G then P : Enter luogu problem details
      - Ctrl+Alt+B : Run all cases in CPH
      - ctrl+D : choose this and dump to the next
      - ctrl+Shift+L : choose all like this
      - ctrl+K then ctrl+W: close all
      - Alt+la/ra : move mouse to pre/nxt pos'

*/
#include <bits/stdc++.h>
#include <queue>
using namespace std;

#define rep(i, l, r) for (int i = l, END##i = r; i <= END##i; ++i)
#define per(i, r, l) for (int i = r, END##i = l; i >= END##i; --i)
#define pb push_back
#define mp make_pair
// #define int long long
#define ull unsigned long long
#define pii pair<int, int>
#define ps second
#define pf first

// #define innt int
#define itn int
// #define inr intw
// #define mian main
// #define iont int

// using fread
#define INPUT_OPTIMIZE

// using fwrite
#define OUTPUT_OPTIMIZE

namespace IO { //新的IO优化

#ifdef INPUT_OPTIMIZE

    #ifdef __unix__
        #include<sys/mman.h>
        inline static const char *_read_ptr = (const char *)mmap(nullptr, 0x7fffffff, 1, 2, 0, 0);
        #define getchar() (*_read_ptr++) 
    #else
        static char buf[1<<21/*如不对请调整buf和fread的长度,write部分同上*/], *p1 = buf, *p2 = buf;
        #define getchar() p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++
    #endif

#endif

    inline bool read(char *t) {
        memset(t, 0, sizeof t);
        char *pd = t, c = getchar();
        while (isspace(c)) c = getchar();
        while (!isspace(c)) *pd++ = c, c = getchar();
        return c == EOF;
    }

    template <typename T> 
    inline bool read(T &t) {
        t=0;T sgn=1;
        char c = getchar();
        while (!isdigit(c)) {if(c == '-')sgn *= -1;c = getchar();}
        while (isdigit(c)) t = (t << 3) + (t << 1) + (c ^ 48), c = getchar();
        t*=sgn;
        return c == EOF;
    }

    template <typename T, typename... Args> 
    inline bool read(T &t, Args&... args) {
        return read(t) ? 1 : read(args...);
    }

#ifdef OUTPUT_OPTIMIZE
    static char outbuf[1<<21], *out = outbuf;
    #define putchar(x) *out++ = x
    #define flush() fwrite(outbuf, 1, out - outbuf, stdout)
#else 
   #define flush() 0
#endif

    template 
    <typename T> 
    inline 
    void write(T x) {
        if (!x) return putchar('0'), void();
        static char t[9], p = 0;
        while (x) t[++p] = (x % 10) ^ 48, x /= 10;
        while (p) putchar(t[p--]);
    }

}

using namespace IO;

#define ell dbg('\n')
const char el='\n';
const bool enable_dbg = 1;
template <typename T,typename... Args>
void dbg(T s,Args... args) {
    if constexpr (enable_dbg){
    cerr << s;
    if(1)cerr<<' ';
        if constexpr (sizeof...(Args))
            dbg(args...);
    }
}

#define zerol = 1
#ifdef zerol
#define cdbg(x...) do { cerr << #x << " -> "; err(x); } while (0)
void err() { cerr << endl; }
template<template<typename...> class T, typename t, typename... A>
void err(T<t> a, A... x) { for (auto v: a) cerr << v << ' '; err(x...); }
template<typename T, typename... A>
void err(T a, A... x) { cerr << a << ' '; err(x...); }
#else
#define dbg(...)
#endif


const int N = 4e6 + 5;
const int INF = 1e18;
const int M = 1e7;
const int MOD = 1e9 + 7;


struct edge{
    int v,id,nxt;
}e[N],g[N];
int h[N],hg[N];
// vector<edge> e[N];
int idx;

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

    e[++idx]={a,id,h[b]};
    h[b]=idx;
}

int dfn[N],tim;

int edcc[N],edccCnt;
int sz[N];
int low[N];


int a[N];


void addg(int a,int b){
    g[++idx]={b,0,hg[a]};
    hg[a]=idx;

    g[++idx]={a,0,hg[b]};
    hg[b]=idx;
}

itn stk[N],top;


void tarjan(int x,int id){
    //edcc
    dfn[x]=low[x]=++tim;
    stk[++top]=x;
    for(int i=h[x];i;i=e[i].nxt){
        auto v=e[i];
        if(!dfn[v.v]){tarjan(v.v,v.id);low[x]=min(low[x],low[v.v]);}
        if(v.id!=id)low[x]=min(low[x],dfn[v.v]);
    }

    if(dfn[x]==low[x]){
        edccCnt++;
        int y;
        do{
            y=stk[top--];
            edcc[y]=edccCnt;
            sz[edccCnt]+=a[y];
        }while(y!=x);
    }


}

int n,m;

namespace LCA{

    int dep[N],fa[N][22],VIS[N];
    int cnt=0;

    void dfs(int x,int f){
        if(VIS[x]) return;
        VIS[x]=1;
        dep[x]=dep[f]+1;
        ++cnt;

        for(int i=hg[x];i;i=g[i].nxt){
            int v=g[i].v;
            if(v==f||g[i].id)continue;
            fa[v][0]=x;
            for(int j=1;j<20;j++){
                fa[v][j]=fa[fa[v][j-1]][j-1];
            }
            dfs(v,x);
        }
    }


    int lca(int u,int v) {
        if(dep[u]<dep[v]) swap(u,v);
        for(int i=19;~i;--i)
            if(dep[fa[u][i]]>=dep[v]) u=fa[u][i];
        if(u==v) return u;
        for(int i=19;~i;--i)
            if(fa[u][i]!=fa[v][i]) u=fa[u][i],v=fa[v][i];
        return fa[u][0];
    }
}using namespace LCA;

int c[N];

// unordered_map<int,map<int,int> > conn;
bitset<N> vis;


void dfs2(int x,int f){
    vis[x]=1;
    for(int i=hg[x];i;i=g[i].nxt){
        int v=g[i].v;
        if(v==f||vis[v])continue;
        dfs2(v,x);
        c[x]+=c[v];
    }
}


void solve(){
    read(n,m);
    for(int i=1;i<=n;i++){
        read(a[i]);
    }

    int u,v;
    for(int i=1;i<=m;i++){
        read(u,v);
        add(u,v,i);
    }

    for(int i=1;i<=n;i++){
        if(!dfn[i])tarjan(i,0);
    }
    idx=0;

    for(int i=1;i<=n;i++){
        for(int j=h[i];j;j=e[j].nxt){
            auto v=e[j];
            if(edcc[i]!=edcc[v.v])addg(edcc[i],edcc[v.v]);
        }
    }


    dfs(1,0);
    int q;
    read(q);

    int x,y;
    while(q--){
        read(x,y);
        x=edcc[x];
        y=edcc[y];

        int anc=lca(x,y);
        // cdbg(x,y,anc);
        c[x]++;
        c[y]++;
        c[anc]--;
        c[fa[anc][0]]--;
    }

    dfs2(1,0);

    int ans=0;

    for(int i=1;i<=edccCnt;i++){
        if(c[i]>0)ans+=sz[i];
    }

    cout<<ans<<endl;
}

signed main() {
    // freopen(".in","r",stdin);
    // freopen(".in","w",stdout);

    int T=1;
    while(T--){
        solve();
    }
    return 0;
}

边双连通分量和点双连通分量的区别

在多数图中,两者看起来没有什么区别。两者的主要区别在于:

若两个边双连通分量之间有1个公共点,则它们组成一个更大的边双连通分量。

若两个点双连通分量之间有2个公共点,则它们组成一个更大的点双连通分量。