浅谈算法_tarjan

概念

说道tarjan,第一时间应该想到神奇海螺它是一种强大的图论算法,说起来其实很简单,tarjan是一种求有向图中的强连通分量的算法(当然也可以拓展至无向图)。
接下来让我们来学习下这种强大的算法。

定义

1.专业名词

1.返祖边:由于原图不是一棵树,所以我们要将有些边删去,但图中又不能失去这些边,于是我们需要将造成dfn次序混乱的边连为返祖边,即返回祖先的边。
2.割点:也叫割顶,一个图内割去图就不连通的点。
3.割边:也叫桥,一个图内割去图就不连通的点。
4.点双:点双连通分量,图内每个点都非割点。
5.边双:边双连通分量,图内每条边都非割点(桥)。

2.需要的数组

1.dfn:时间戳,即每一个节点的dfs序1
2.low:子树内dfn的最小值,即返祖边可以到达的最高的节点。
3.stack:我们需要的栈。
4.bz:判断一个点是否在栈中。

大致思想

可能某些地方讲的不是很清楚,有什么问题可以提出来,随时欢迎大佬打扰~

1.求强连通分量

从某个点u出发

1.如果从u出发没有点可以搜了并且low[u]==dfn[u]:
那么说明我们已经遍历了这个点所属的连通分量并且这个点就是该连通分量的根(我们的算法也保证了在搜索过程中该连通分量上的每个点的low值都更新为该连通分量的根),由于该连通分量已经确定了,所以我们可以把以u点为根的连通分量从图中删掉。

2.如果搜到的下一个节点v未被访问过(可以由dfn来判断):
那么就把它进栈并从它出发进行深搜。

3.如果搜到的下一个点v被访问过并且在栈中(也就是bz[i]=1,那些被访问过但已经出栈的节点所在的连通分量已经确定了,所以可以忽略这些点):
那么说明从v到u这条链上的点都属于一个连通分量,这个时候就应该更新每个节点的low值为这个连通分量中时间戳最小的点的low值,在回溯的过程中也不断将这个最小的low值传递。

2.求边双

1.先求割边:
首先,我们分析一下桥的特点,删除一条边之后,那么如果dfs过程中的子树没有任何一个点可以到达父亲节点及父亲节点以上的节点,那么这个时候子树就被封死了,这条边就是桥。

2.有了这个性质,也就是说当我们dfs过程中遇到一条树边u->v,并且此时low[v]>dfs[u],那么a-b就是一座桥。

3.证明:
∵low的性质
∴u子树内的节点的返祖边一定高度不超出u
∴u与father[u]之间的边使u的子树与u的父亲隔绝
∴u与father[u]之间的边是一条割边
证毕,至此,我们找到了桥。

4.我们把所有的桥去掉之后那些独立的分量就是不同的边双连通分量,这个时候就可以按照需要灵活的求出边双连通分量了。

5.low值相同的点一定存在同个边双(不会证明哪位大佬补充下,谢谢)。

3.求点双

1.先求割点:
若low[v]>=dfn[u],则u为割点。

2.证明:
∵low的性质
∴u子树内的节点的返祖边一定高度不超出u
∴u点使u的子树与u的祖先节点们隔绝
∴u点是一个割点
证毕,至此,我们找到了割点。

3.在求割点的过程中就能把每个点双连通分支求出。建立一个栈,存储双连通分支。

4.在搜索图时,每找到一条树枝边或后向边(非横叉边),就把这条边加入栈中2

5.如果遇到某时满足dfn(u)<=low(v),说明u是一个割点,同时把边从栈顶一个个取出,直到遇到了边(u,v),取出的这些边与其关联的点,组成一个点双连通分支。

6.点双有一个性质,即同一个点可以属于不同的点双,但同一条边则不可能属于同个点双3

注意

如下图:
这里写图片描述
在这幅图中,整幅图是边双,但却是两个点双,中间与上面两个点属于一个点双,中间与下面两个点又属于一个点双。

代码

有些地方打得丑,见谅。

1.强连通分量

int tarjan(int u)
{
    dfn[u]=low[u]=++x;
    stack[++stack[0]]=u,bz[u]=1;//加入栈中
    for (int i=last[u];i;i=next[i])
        if (!dfn[tov[i]])
        {
            tarjan(tov[i]);
            low[u]=min(low[u],low[tov[i]]);//更新low
        }
        else
            if (bz[tov[i]]) 
                low[u]=min(low[u],low[tov[i]]);//更新low
    if (dfn[u]==low[u])//建立强连通分量
    {
        for(;stack[stack[0]]!=u;stack[0]--) 
            bz[stack[stack[0]]]=0;
        bz[stack[stack[0]--]]=0;
    }
}

2.边双

int tarjan(int u,int fa)
{
    int i;
    x++;
    dfn[u]=low[u]=x;
    stack[++stack[0]]=u;
    for (i=last[u];i;i=next[i])
        if (!dfn[tov[i]])
        {
            tarjan(tov[i],u);
            low[u]=min(low[u],low[tov[i]]);
            if (low[tov[i]]>dfn[u])
            {
                m++;//存储桥
                if(tov[i]<u) 
                    bridge[m][0]=tov[i],bridge[m][1]=u;
                else 
                    bridge[m][1]=tov[i],bridge[m][0]=u;
            }
        }
        else
            if (tov[i]!=fa) 
                low[u]=min(low[u],dfn[tov[i]]);
    if (low[u]==dfn[u])
    {
        tot++;
        for(;(stack[0])&&(stack[stack[0]+1]!=u);stack[0]--) //求边双
            belong[stack[stack[0]]]=tot;
    }
}

3.点双

int tarjan(int u)
{
    int i,xx;
    tot++;
    dfn[u]=low[u]=tot;
    stack[++stack[0]]=u;//加入栈顶
    for (i=last[u];i;i=next[i])
        if (!dfn[tov[i]])
        {
            tarjan(tov[i]);
            low[u]=min(low[u],low[tov[i]]);//更新low
            if (low[tov[i]]>=dfn[u])//找到割点
            {
                cnt++,xx=0;
                for(;stack[0] && xx!=tov[i];stack[0]--)//以下为建圆方树,如不需要可以删去连边(此link为在新建图上连边)
                {
                    xx=stack[stack[0]];
                    link(xx,cnt);
                    link(cnt,xx);
                }
                link(u,cnt);
                link(cnt,u);
            }
        }
        else 
            low[u]=min(low[u],dfn[tov[i]]);//更新low
}

有什么问题或建议欢迎提出~

本文特别鸣谢:CZC


  1. 顾名思义,就是在DFS后得到的序列,可用作DP时消去后效性。
  2. 这里可以加入边连向的点,方便取出。
  3. 证明如下:
    利用反证法:
    当一条边属于不同点双时,它所在的两个点双,一定可以合成一个大点双,得证。
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值