Tarjan全家桶

简介

Tarjan主要用来求强连通分量的,假如说对于一个有向图G,这个图中每一个点都能够互相到达,那么G就是一个强连通图。对于一个有向图中的极大强连通子图就是强连通分量。(即有向图中的一个环就是一个强连通分量)
如图,一种颜色的点就是一个强连通分量。
在这里插入图片描述

算法步骤

我们将有向图变成一棵搜索树。如下图(和上图不一样)。
在这里插入图片描述
如图,蓝色的为返祖边,红色的为横叉边。
我们对于每个点设DFN[i]为遍历到这个点的时间戳,LOW[i]为遍历到的最早的时间戳。假如说搜到了一个返祖边(如边(7,3)),证明3,6,7三点可以互相到达,所以{3,6,7}为强连通分量。
所以我们开一个栈来储存环中的点,如果找到强连通分量,就要将强连通分量中的所有点退掉。
假如遇到了横叉边(i,j),假如栈内有点j,那么就要更新LOW[i]的值,否则不更新。
当DFN[i]=LOW[i]的时候,证明栈顶到当前点这一段都是属于当前点所属的这个环的,将这个环中的所有点退掉。

Code

图1

void tarjan(int x)
{
    h++;
    dfn[x]=low[x]=h;
    bz[x]=1;
    st[++top]=x;
    int i,now;
    for (i=head[x];i;i=next[i])
    {
        now=go[i];
        if (dfn[now]==-1)
        {
            tarjan(now);
            low[x]=min(low[x],low[now]);
        } else
        if (bz[now]) low[x]=min(low[x],dfn[now]);
    }
    if (dfn[x]==low[x])
    {
        cnt++;
        while (st[top+1]!=x)
        {
            bz[st[top]]=0;
            g[st[top]]=cnt;
            top--;
        }
    }
}

全家桶

在后面两年的OI学习过程中,我对tarjan又有了新的理解。
tarjan需要非常熟练,否则做tarjan题的时候十分吃亏。
接下来便介绍一下tarjan的其他经典操作。

点双与边双

无向图中:
点双定义:去掉点双中任意一点都不会改变该图的连通性。
边双定义:去掉边双中任意一边都不会改变该图的连通性。
点双性质:任意两个点之间都可找到两条不重复的路径,可以理解为若干个有相交的环。割点,如果去掉它,这个图就不连通了。
边双性质:任意两个点之间都可找到两条不重复的路径,可以理解为若干个有相交的环。桥,如果去掉它,这个图就不连通了。

判断点是否在栈中?

只有在有向图中才需要判断。
判断点是否在栈中,实质上是判断有向图中的横叉边。而无向图中没有横叉边,所以不用bool数组判断。

求割点及桥

利用dfn序和low来求得。
割点:如果dfs树上存在一条边(x,y),其中x是y的dfs树的父亲,使得 l o w [ y ] > = d f n [ x ] low[y]>=dfn[x] low[y]>=dfn[x],那么x必为割点。
dfs树的根z是否为割点?如果它有多个儿子,且存在一对儿子的low不同,那么z为割点。
桥:如果dfs树上存在一条边(x,y),其中x是y的dfs树的父亲,使得 l o w [ y ] > = d f n [ y ] ( 即 l o w [ y ] = d f n [ y ] ) low[y]>=dfn[y](即low[y]=dfn[y]) low[y]>=dfn[y](low[y]=dfn[y]),则(x,y)为桥。
那么非dfs树的边呢?一定不是桥。
无向图中,边只有树边和非树边,而非树边必为返祖边,那么必然产生环。

求点双和边双

求点双中的点

当x不为dfs树根时:
如果 l o w [ y ] > = d f n [ x ] low[y]>=dfn[x] low[y]>=dfn[x],则x必为割点,栈中的点一定在点双中。直接弹栈即可。
当x为dfs树根时:
x不一定为割点,但栈中的点一定在点双中。直接弹栈即可。
综上所述,直接弹栈即可。
图2

void tarjan(int x){
 int i;
 Dfn[x]=Low[x]=++T;tar[T]=x;
 st[++st[0]]=x;
 for(i=head[x];i;i=edge[i].next)
     if(!Dfn[edge[i].to]){
      tarjan(edge[i].to);
      v=edge[i].to;
      Low[x]=min(Low[x],Low[v]);
      if(Low[v]>=Dfn[x]){
       CNT++;
       while(v^st[st[0]+1]){
        Bz[st[st[0]]]=0;
        st[0]--;
    }
   }
  }else
   Low[x]=min(Low[x],Dfn[edge[i].to]);
}

求边双中的点

如果 l o w [ y ] > = d f n [ x ] low[y]>=dfn[x] low[y]>=dfn[x],那么直接弹栈即可。
当然了,像图1的代码一样也行。
图3

void tarjan(int x,int y){
 int i;
 dfn[x]=low[x]=++T;
 st[++top]=x;
 for(i=head[x];i;i=edge[i].next){
  if(edge[i].id==y)continue;
  if(!dfn[edge[i].to]){
      tarjan(edge[i].to,edge[i].id);
      low[x]=Min(low[x],low[edge[i].to]);
  }else low[x]=Min(low[x],dfn[edge[i].to]);
 }
 if(dfn[x]==low[x]){
        CNT++;
        while(st[top+1]^x){
         bel[st[top]]=CNT;
         top--;
  }
 }
}

缩点双和边双之后

维护点双和边双的信息。我们希望将图转化为树。

点双

圆方树。
圆点代表原图中的点,方点代表点双。
每个方点连向该点双中的点,最终会成为一棵树。
如果根深度为1且为圆点,那么奇数深度的点为圆点,偶数深度的点为方点。
方点与圆点的信息密切相关。

边双

缩掉边双之后,直接就成为了一棵树。
维护树上的信息即可。

例题

JZOJ5909
POJ3352

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值