tarjan

tarjan算法求有向图的强连通分量


连通性的相关知识

无向图的连通分量

在无向图中,如果从节点 v i v_i vi到节点 v j v_j vj有路径,则称节点 v i v_i vi和节点 v j v_j vj是连通的。如果图中任意两个节点之间都是连通的,则称图 G G G为连通图。

如下图所示:

image-20210730140441026

无向图 G G G极大连通子图被称为图 G G G的连通分量。极大连通子图是图 G G G的连通子图,如果再向其中加入一个节点,则该子图就不再是连通的。连通图的连通分量就是它自身;非连通图则有两个及以上的连通分量。

如下图所示,有3个连通分量:

image-20210730141251304

有向图的强连通分量

在有向图中,如果图中的任意两个节点从 v i v_i vi v j v_j vj都有路径,且从 v j v_j vj v i v_i vi也有路径,则称图 G G G为强连通图。

有向图 G G G极大强连通子图被称为图 G G G的强连通分量。极大强连通子图是图 G G G的强连通子图,如果再向其中加入一个节点,则该子图就不再是强连通的。

如下图所示,(a)是强连通图,(b)不是强连通图,©是(b)的强连通分量:

image-20210730141824251


tarjan算法

首先引入时间戳和追溯点的概念

  • 时间戳:dfn[u]表示节点 u u u深度优先遍历的序号(也就是节点 u u u被访问的时间点)
  • 追溯点:low[u]表示节点 u u u或者节点 u u u的子孙能够通过非父子边追溯到的dfn最小的节点序号,即回到最早的过去(也就是节点 u u u通过有向边可回溯到的最早的时间点)

这里有必要说以下dfs遍历的两种方式:

  • 方式一:先访问当前节点,然后再递归访问相邻节点(这类似于树的先序遍历)
  • 方式二:先递归相邻节点,到达叶子节点后回溯时再依次访问路径中的节点(这类似于树的后序遍历)

如下图所示,这两种方式输出的结果是不同的:

image-20210730143214955

在tarjan算法中,采用的是方式二

举个栗子,在深度优先搜索中,每个点的时间戳和追溯点的求解过程如下:

我们用栈stk来存储访问的节点

image-20210730145108651

初始时, d f n [ u ] = l o w [ u ] dfn[u]=low[u] dfn[u]=low[u],从节点 1 1 1开始深搜,如果该节点的邻接点还没有被访问过,则一直递归进行深度优先遍历, 1 → 2 → 3 → 5 → 6 → 4 1\to2\to3\to5\to6\to4 123564,此时栈中内容为{1,2,3,5,6,4},此时节点 4 4 4的邻接点 1 1 1已经被访问过了,并且节点 1 1 1并不是节点 4 4 4的父节点,节点 4 4 4的父节点是节点 6 6 6(深度优先搜索树上的父节点),那么这条边 4 → 1 4\to1 41就是一条非父子边,那么节点 4 4 4顺着这条非父子边能回到最早的节点是节点 1 1 1,那么此时就需要修改节点 4 4 4它的追溯点为 d f n [ 1 ] dfn[1] dfn[1]。我们知道 d f n [ 1 ] dfn[1] dfn[1]必然是小于 l o w [ 4 ] low[4] low[4]的,因为节点 1 1 1先于节点 4 4 4被访问,它的时间戳小。所以我们可以直接用 l o w [ 4 ] = d f n [ 1 ] low[4]=dfn[1] low[4]=dfn[1],但是为了严谨,一般都是用 l o w [ 4 ] = m i n ( l o w [ 4 ] , d f n [ 1 ] ) low[4]=min(low[4],dfn[1]) low[4]=min(low[4],dfn[1])

但是有个问题,设当前访问节点 u = 4 u=4 u=4,遍历到它的邻接点 v = 1 v=1 v=1,要执行 l o w [ u ] = m i n ( l o w [ u ] , d f n [ v ] ) low[u]=min(low[u],dfn[v]) low[u]=min(low[u],dfn[v])的前提是节点 v v v它此刻还是在栈中,为什么呢?

如上图所示,当节点 4 4 4要访问节点 1 1 1时,此时栈内的元素为{1,2,3,5,6,4},可以发现节点 v = 1 v=1 v=1还在栈中,也就是说节点 u = 4 u=4 u=4可以通过这条非父子边到达节点 v = 1 v=1 v=1。如果节点 v = 1 v=1 v=1不在栈中,那么可以认为此时节点 u = 4 u=4 u=4与节点 v = 1 v=1 v=1这条非父子边是不存在的。既然非父子边不存在,那么就不能通过非父子边回溯到更早,于是也就不能更新节点 u u u的时间戳。

在上面分析中,我们知道了 l o w [ 4 ] = m i n ( l o w [ 4 ] , d f n [ 1 ] ) = 1 low[4]=min(low[4],dfn[1])=1 low[4]=min(low[4],dfn[1])=1,接下来就要开始回溯了。回溯时需要执行 l o w [ u ] = m i n ( l o w [ u ] , l o w [ v ] ) low[u]=min(low[u],low[v]) low[u]=min(low[u],low[v]),为什么呢?因为既然子孙能够回到更早过去,那么其祖先节点也可以回到更早的过去,于是回溯时此刻 u = 6 , v = 4 u=6,v=4 u=6,v=4,更新 l o w [ 6 ] = m i n ( l o w [ 6 ] , l o w [ 4 ] ) = 1 low[6]=min(low[6],low[4])=1 low[6]=min(low[6],low[4])=1,以此类推,更新节点 5 , 3 , 2 5,3,2 5,3,2的时间戳。

我们可以知道{1,2,3,5,6,4}是一个强连通分量了。同理分析,从节点 1 1 1开始深搜,如果该节点的邻接点还没有被访问过,则一直递归进行深度优先遍历, 1 → 2 → 3 → 5 → 7 1\to2\to3\to5\to7 12357,此时栈中内容为{1,2,3,5,7}。当前节点 u = 7 u=7 u=7,可以知道它没有非父子边,所以它不能通过非父子边回溯到更早的过去。于是它自己就是一个强连通分量{7}。

image-20210730151852176

那么我们什么时候才能输出这个强连通分量呢?

当回溯时,一直回溯到某个节点,它的时间戳dfn[u]与追溯点low[u]是相同时,就可以输出这个强连通分量了。因为此时说明这个节点 u u u它不再能够通过非父子边回溯到更早的过去了。


代码

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1e5+10,M=1e6+10;
int n,m;
int h[N],e[M],ne[M],idx;
//用栈stk来存储强连通分量中的节点
int stk[N],top;
//num是时间戳
int dfn[N],low[N],num;
//scc_cnt是记录这是第几个强连通分量
//id数组用来存储某个节点是属于哪个强连通分量
//cnt用来记录某个强连通分量中的节点个数
int id[N],cnt[N],scc_cnt;
//判断某个节点是否在栈中
bool in_stk[N];

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

void tarjan(int u)
{
    dfn[u]=low[u]=++num;
    stk[++top]=u;
    in_stk[u]=true;
    printf("调用tarjan(%d) dfn[%d]=low[%d]=%d,栈顶stk[%d]=%d\n",u,u,u,num,top,u);
    for(int i=h[u];~i;i=ne[i])
    {
        int j=e[i];
        if(!dfn[j])
        {
            tarjan(j);
            low[u]=min(low[u],low[j]);
            printf("回溯 %d->%d low[%d]=%d\n",j,u,u,low[u]);
        }
        else if(in_stk[j])
        {
            low[u]=min(low[u],dfn[j]);
            printf("有环 %d->%d low[%d]=%d\n",u,j,u,low[u]);
        }
    }
    printf("判断if(%d==%d)?\n",dfn[u],low[u]);
    if(dfn[u]==low[u])
    {
        scc_cnt++;
        int y;
        printf("输出第%d个强连通分量:{",scc_cnt);
        do{
            y=stk[top--];
            in_stk[y]=false;
            id[y]=scc_cnt;
            cnt[scc_cnt]++;
            printf("%d ",y);
        }while(y!=u);
        printf("}\n输出此时栈顶stk[%d]=%d\n\n",top,stk[top]);
    }
}
int main()
{
    memset(h,-1,sizeof h);
    cin >>n>>m;
    while(m--)
    {
        int a,b;
        cin >>a>>b;
        add(a,b);
    }
    for(int i=1;i<=n;i++)
    {
        if(!dfn[i])
            tarjan(i);
    }
}

例如上图中的栗子来说,输出结果如下:

image-20210730152417469

  • 第一个连通分量为{7}
  • 第二个连通分量为{4,6,5,4,2,1}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卷心菜不卷Iris

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值