求解边双联通分量的方法

本文介绍了如何求解边双连通分量,重点在于避免在搜索过程中回溯已通过的边。通过为每对双向边编号,并记录每个节点的进入边编号,确保在DFS遍历过程中不重复经过相同边,从而完整遍历所有边双连通分量。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我们在求解边双连通分量的时候,只需要把每个点从哪条边走来进行记录就行了

所谓边双连通分量是指不存在桥的一个子图,也可以说从一个点到另一个点一定是有两条边不相交的路的

这里写图片描述

对于这样一张图,我们要如何去求出其中的边双连通分量?我们会发现,对于这一整张图,任意两点都可以互达并且路径无边相交,并且图中显然不存在桥
如果是普通的tarjan算法,对于双向边,会搜回去,所以我们限制其不能走反向边,加入DFS的顺序是ABCDE,DFS遍历图是这样的顺序:
这里写图片描述

给每一对双向边编一个序号
操作如下:

void add_line(int from,int to){
    tail++;
    line[tail].from=from;
    line[tail].to=to;
    line[tail].nxt=head[from];
    head[from]=tail;
    tail++;
    line[tail].from=to;
    line[tail].to=from;
    line[tail].nxt=head[to];
    head[to]=tail;
    cc++;
    line[tail].cnn=cc;
    line[tail-1].cnn=cc;
}

也就是最后两行在给每一对双向边编号

这里写图片描述

假如在插入边的时候是按照如图所示的 第一次插入的是A->B,第二次是B->C…etc.
那么我们把fa[B]定义为1,把fa[C]定义为2(也即从哪条边走来),这样在forC的子点的时候,我们会for到B,而这条边的双向边编号是2,此时遇到fa[C]=该子点与C的边的双向边编号,所以我们不走它
也即下面的这小段代码

for(register int i=head[u];i;i=line[i].nxt){
        int v=line[i].to;
        if(line[i].cnn==fa[u]) continue;
        if(!dfn[v]){
            fa[v]=line[i].cnn;
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }else{
            low[u]=min(low[u],dfn[v]);
        }
    }

我们可以发现,其实其和求强连通分量的差别只是不能倒回去经过刚才已经经过过的边,这样一来我们完整地从沙漏图的A走到B走到C走到D走到E,而遍历所有的边,虽然C这个点经过了两次,但是我们成功地遍历完所有的边而没有因为重复而只能遍历上半部分或者下半部分,如果按照强连通分量的遍历方法,我们会发现在在tarjan(C)点的时候,遍历C点下面的子树,依次遍历D点和E点,然后会回到C点,最后退出来的时候我们会发现C点D点E点形成了一个强连通分量???233所以我们要把一个点是从哪里走过来的打上标记
完整代码:

#include<cstdio>
#include<cstring>
#include<iostream>
#define MAXN 100000
using namespace std;
struct Line{
    int from,to,nxt,cnn;
}line[MAXN];
int head[MAXN],stack[MAXN],top,tail,cc,fa[MAXN],low[MAXN],dfn[MAXN],tim,cnt,scc[MAXN],out[MAXN],n,m,from,to;
bool vis[MAXN];
void add_line(int from,int to){
    tail++;
    line[tail].from=from;
    line[tail].to=to;
    line[tail].nxt=head[from];
    head[from]=tail;
    tail++;
    line[tail].from=to;
    line[tail].to=from;
    line[tail].nxt=head[to];
    head[to]=tail;
    cc++;
    line[tail].cnn=cc;
    line[tail-1].cnn=cc;
}
void tarjan(int u){
    tim++;
    dfn[u]=low[u]=tim;
    stack[++top]=u;
    for(register int i=head[u];i;i=line[i].nxt){
        int v=line[i].to;
        if(line[i].cnn==fa[u]) continue;
        if(!dfn[v]){
            fa[v]=line[i].cnn;
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }else{
            low[u]=min(low[u],dfn[v]);
        }
    }
    if(dfn[u]==low[u]){
        cnt++;
        scc[u]=cnt;
        while(stack[top]!=u){
            scc[stack[top]]=cnt;
            top--;
        }
        top--;
    }
}
int main(){
    //freopen("1718.txt","r",stdin);
    //freopen(".out","w",stdout);
    scanf("%d%d",&n,&m);
    for(register int i=1;i<=m;i++){
        scanf("%d%d",&from,&to);
        add_line(from,to);
    }
    for(register int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);
    for(register int i=1;i<=n;i++){
        for(register int j=head[i];j;j=line[j].nxt){
            int v=line[j].to;
            if(vis[j])continue;
            if(scc[i]==scc[v])continue;
            out[scc[i]]++; out[scc[v]]++;
            vis[j]=true;
        }
    }
    int final=0;
    for(register int i=1;i<=cnt;i++)
        if(out[i]==2) final++;
    printf("%d\n",(final+1)>>1);
    return 0;
}

如果还有不懂的,请自行模拟上面所画的沙漏图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值