Loading

【笔记】双连通分量

双连通分量

割边:无向图中,删除这条边会使图不连通的边(又叫桥)。

割点:无向图中,删除这个点会使图不连通的点。

边双连通:两点 \(u,v\) 间不存在割边,就称 \(u,v\) 边双连通。即:删去 \(u,v\) 间任意一边,\(u,v\) 仍然连通。

点双连通:两点 \(u,v\) 间不存在割点,就称 \(u,v\) 点双连通。即:删去 \(u,v\) 间任意一点,\(u,v\) 仍然连通。

边双连通分量(e-DCC):无向图中的极大边双连通子图。

点双连通分量(v-DCC):无向图中的极大点双连通子图。

Tarjan

无向图割边割点、双连通分量均可以使用 Tarjan 算法高效求解。

求割边

对于一条边 \((u,v)\),若 \(v\) 无法回溯到 \(u\) 及以前搜到的节点,则若删去 \((u,v)\)\(u\)\(v\) 就无法连通,因此 \((u,v)\) 是一条割边。

在 Tarjan 算法中,注意更新 \(low_u\) 时跳过反向边,然后判断 \(low_v>dfn_u\) 即可。

void tarjan(int u,int fa){
    low[u]=dfn[u]=++dfncnt;
    for(int i=h[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(!dfn[v]){
            tarjan(v,i);
            if(low[v]>dfn[u]) ans[++idx].from=u,ans[idx].to=v;
            low[u]=min(low[u],low[v]);
        }else if((i^1)!=fa){//判断反向边。需要链式前向星 tot 从 2 开始
            low[u]=min(low[u],dfn[v]);
        }
    }
    return ;
}

求割点

同理,对于一点 \(u\),若其一子节点 \(v\) 不能回溯到 \(u\) 之前搜到的节点,则 \(u\) 是个割点。只需要把 \(low_v>dfn_u\) 改为 \(low_v\ge dfn_u\) 即可。

特例是 DFS 树的根节点,如果在 DFS 树上根只有一个儿子,那么删除根剩下的节点依然连通。当儿子数 \(\ge2\) 时,根才是割点。

但要注意 \(u\) 可能不止一个子节点满足以上条件,会多次算出 \(u\)

void tarjan(int u,int fa){
    low[u]=dfn[u]=++dfncnt;
    int cnt=0;
    for(int i=h[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(!dfn[v]){
            tarjan(v,i);
            if(low[v]>=dfn[u]&&fa!=-1) vis[u]=1;
            low[u]=min(low[u],low[v]);
            cnt++;
        }else if((i^1)!=fa){//判断反向边。需要链式前向星 tot 从 2 开始
            low[u]=min(low[u],dfn[v]);
        }
    }
    if(fa==-1&&cnt>1) vis[u]=1;
    return ;
}

求 e-DCC

对于一个无向图,由于我们已确定搜索开始的点,整张图的 DFS 搜索树就已经确定下来。此时删掉所有 DFS 搜索树上边的反向边,构造出一个有向图。显然,有向图的 SCC 等价于无向图的 e-DCC。

并且,由于无向边相邻两点互相可达,图上便不存在(对于 DFS 树的)横叉边,因为两点若有边相连,两点均可成为对方的祖先,所以搜索时总是会先构成祖先关系。所以只存在返祖边,自然不用判断走到的已访问的点是否在栈中。

int dfn[N],low[N],dfncnt,st[N],top;
int dcc[N],dc;//结点 i 所在 eDCC 的编号,eDCC 计数
int sz[N];//eDCC i 的大小
void tarjan(int u,int fa) {
    low[u]=dfn[u]=++dfncnt,st[++top]=u;
    for(int i=h[u];i;i=e[i].nxt) {
        int v=e[i].to;
        if(!dfn[v]){
            tarjan(v,i);
            low[u]=min(low[u],low[v]);
        }else if((i^1)!=fa){//判断反向边。需要链式前向星 tot 从 2 开始
            low[u]=min(low[u],dfn[v]);
        }
    }
    if(dfn[u]==low[u]) {
        dc++;
        while(st[top]!=u) {
            dcc[st[top]]=dc;
            sz[dc]++;
            top--;
        }
        dcc[st[top]]=dc;
        sz[dc]++;
        top--;
    }
}

求 v-DCC

一个图是点双连通图,当且仅当:点的个数不超过 \(2\) 或图是一个简单环(不是由多个小环交叉拼成的,最普通的环)。

不同于 e-DCC 去掉了割边,如果整张图不是点双连通图,那么每个 v-DCC 都包含割点,且割点可能属于多个 v-DCC 中,非割点一定属于一个 v-DCC 中。

在 Tarjan 算法求出割点 \(u\) 时,我们把栈中 \(u\) 以上的节点全部弹出。这些节点是 \(u\) 能访问到的,且回溯不到 \(u\) 之前但能回溯到 \(u\) 的所有点,这是因为回溯不到 \(u\) 的会在求出 \(u\) 之后搜到的某个割点时就出栈。但是 \(u\) 不一定只属于一个 v-DCC,所以不能把 \(u\) 出栈。

另外,孤点也需要统计。

int dfn[N],low[N],dfncnt;
int st[N],top;
vector<int> g[N];
void tarjan(int u,int fa){
    dfn[u]=low[u]=++dfncnt;
    st[++top]=u;
    int cnt=0;
    for(int i=h[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(!dfn[v]){
            cnt++;
            tarjan(v,i);
            low[u]=min(low[u],low[v]);
            if(low[v]>=dfn[u]){
                num++;
                while(st[top]!=v&&top){//弹出 v 及以上,不弹 u
                    g[num].push_back(st[top]);
                    top--;
                }
                g[num].push_back(v);
                top--;
                g[num].push_back(u);
            }
        }else if((i^1)!=fa){//判断反向边。需要链式前向星 tot 从 2 开始
            low[u]=min(low[u],dfn[v]);
        }
    }
    if(fa==-1&&!cnt) g[++num].push_back(u);//孤点
    return;
}
posted @ 2026-02-27 18:35  Seqfrel  阅读(9)  评论(0)    收藏  举报