Tarjan 算法介绍及用法

14 篇文章 0 订阅
9 篇文章 0 订阅

Tarjan

简介:

  • 这是一个有关图联通的算法,它基于dfs 在解决有环的有向图或无向图的问题时,很多算法不好是操作…
  • 那么就先要将进行缩点,将其转换为DAG(有向无环图)或一棵,然后问题应会迎刃而解

常规操作:

首先补几个概念:
- 强连通:在一个DAG中,有 a,b 两点,若a可以到达b且b可以到达a,则(a,b)即为强连通
- 强连通图:若在一个DAG中,任意两点都为强连通,则此图为强连通图
- 强连通分量:在一个DAG中,有一个子图,若该子图为强连通图,则该子图叫做强连通分量

再来补几个数组的含义:
- dfn:dfn[i]表示第i个点的时间编号
- low:low[i]表示以i为根的子树(图)下的最小的时间编号 若dfn[i]==low[i]
- 即该点为一个强连通分量子树(图)的根

算法的核心思想:
通过栈来维护强连通,具体细节还要分类而论…

具体用法:

常规的用法:缩点,割点,割边,点双,边双

缩点:
概念:化环为点
原理:通过栈来将环内存下,并在第二次访问到该环的根节点时,依次弹出,进行编号
模板(code):

1.在有向图中:

void tarjan(int x){
    dfn[x]=low[x]=++tim;
    stk[++top]=x;
    vis[x]=1;
    SREP(i,0,E[x].size()){
        int y=E[x][i];
        if(!dfn[y]){
            tarjan(y);
            chkmin(low[x],low[y]);
        }
        else if(vis[y]) chkmin(low[x],dfn[y]);
    }
    if(dfn[x]==low[x]){
        tot++;
        do{
            Id[stk[top]]=tot;
            vis[stk[top]]=0;
        }while(x!=stk[top--]);
    }
}

2.在无向图中:

void tarjan(int x,int f){
    dfn[x]=low[x]=++tim;
    stk[++top]=x;
    vis[x]=1;
    bool flag=1;
    SREP(i,0,E[x].size()){
        int y=E[x][i];
        if(y==f && flag){flag=0;continue;}
        if(!dfn[y]){
            tarjan(y,x);
            chkmin(low[x],low[y]);
        }
        else if(vis[y]) chkmin(low[x],dfn[y]);
    }
    if(dfn[x]==low[x]){
        tot++;
        do{
            Id[stk[top]]=tot;
            vis[stk[top]]=0;
        }while(x!=stk[top--]);
    }
}

然而一般题目多是无向图,接下来都用无向图了…

割点:
概念:若删掉某点后,原连通图分裂为多个子图,则称该点为割点。
原理:若low[y]>=dfn[x],则x为割点。因为low[y]>=dfn[x],则说明y通过子孙无法到达x的祖先。那么对于原图,去掉x后,必然会分成两个子图。

模板(code):

void tarjan(int x,int f){
    dfn[x]=low[x]=++tim;
    stk[++top]=x;
    bool flag=1;
    int child=0;
    SREP(i,0,E[x].size()){
        int y=E[x][i];
        if(y==f && flag){flag=0;continue;}
        if(!dfn[y]){
            child++;
            tarjan(y,x);
            chkmin(low[x],low[y]);
            if(low[y]>=dfn[x]){
                cut[x]=1;
                //该点为割点 
            }
        }
        else chkmin(low[x],dfn[y]);
    }
    if(!f && child==1)cut[x]=0;//注意初始点,特判 
}

割边(桥):
概念:删掉该边后,原图必然会分裂为多个子图
原理:若low[y]>dfn[x],则边(x,y)为桥。由割点同理可得。但是由于可能存在重边,需要把一条无向边拆成的两条标号相同的有向边,记录每个点的父亲到它的边的标号,如果边(x,y)是x的后向边,就不能用dfn[u]更新low[v]。这样如果遍历完v的所有子节点后,发现low[y]=dfn[y],说明x的后向边(x,y)为割边。
模板(code):

void tarjan(int x,int f){
    dfn[x]=low[x]=++tim;
    stk[++top]=x;
    bool flag=1;
    SREP(i,0,E[x].size()){
        int y=E[x][i];
        if(y==f && flag){flag=0;continue;}
        if(!dfn[y]){
            child++;
            tarjan(y,x);
            chkmin(low[x],low[y]);
            if(low[y]>dfn[x]){
                mark[x][y]=mark[y][x]=1; 
                //该边为割边 
            }
        }
        else chkmin(low[x],dfn[y]);
    }
}

点双(点双连通分量):
概念:在求出所有的割点以后,把所有的割点删除,原图变成了多个连通块。
原理:在求割点的过程中就能把每个点双连通分支求出。通过栈存储双连通分支。在搜索图时,每找到一条后向边(非横叉边),就把这条边加入栈中。如果遇到某时满足dfn[x]<=low(y),说明x是一个割点,同时把边从栈顶一个个取出,直到遇到了边(x,y),取出的这些边与其关联的点,从而组成一个点双,一般再用个容器存一下每个点双。
模板(code):

void tarjan(int x,int f){
    dfn[x]=low[x]=++tim;
    stk[++top]=x;
    vis[x]=1;
    bool flag=1;
    int child=0;
    SREP(i,0,E[x].size()){
        int y=E[x][i];
        if(y==f && flag){flag=0;continue;}
        if(!dfn[y]){
            child++;
            tarjan(y,x);
            chkmin(low[x],low[y]);
            if(low[y]>=dfn[x]){
                cut[x]=1; 
                V[++tot].clear();
                do{
                    vis[stk[top]]=0;
                    Id[stk[top]]=tot; 
                    V[tot].pb(stk[top]);
                }while(y!=stk[top--]);
                V[tot].pb(x);

                solve();//解决该点双上的问题 

            }
        }
        else if(vis[y])chkmin(low[x],dfn[y]);
    }
    if(!f && child==1)cut[x]=0;
}

边双(边双连通分量):
概念:在求出所有的割边以后,把所有的割边删除,原图变成多个连通块。不属于任何一个边双连通分支,其余的边和每个顶点都属于且只属于一个边双连通分支。
原理:与点双同理,在求割边的过程中就能把每条边双连通分支求出,也是通过栈,不过此时自然是存边了,而不是点,之后栈的弹出边界与点双也是同理的,这里就不再赘述。
模板(code):

void tarjan(int x,int fa){
    dfn[x]=low[x]=++tim;
    LREP(x){
        int y=E[i].y;
        if(E[i].flag||dfn[y]>=dfn[x])continue;
        E[i].flag=E[i^1].flag=1;
        stk[++top]=i;
        if(!dfn[y]){
            tarjan(y,x);
            low[x]=min(low[x],low[y]);
            if(low[y]>dfn[x]){
                tot++;
                do{
                    int k=stk[top];
                    if(mark[E[k].y]!=tot){
                        em[++cnt]=make_pair(tot,E[k].y);
                        mark[E[k].y]=tot;
                    }
                    if(mark[E[k^1].y]!=tot){
                        em[++cnt]=make_pair(tot,E[k^1].y);
                        mark[E[k^1].y]=tot;
                    }
                    Id[k>>1]=tot;
                }while(i!=stk[top--]);
            }
        } 
        else low[x]=min(low[x],dfn[y]);
    }
}

总结:

tarjan是一个非常使用且易记忆的算法,将一张有环图转换为DAG一棵树后,
也就变成了一个十分常规的问题了,然后就可以再运用合理的数据结构或算法将其迎刃而解!

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
LCA(最近公共祖先)是指在一棵树中,找到两个节点的最近的共同祖先节点。而Tarjan算法是一种用于求解强连通分量的算法,通常应用于有向图中。它基于深度优先搜索(DFS)的思想,通过遍历图中的节点来构建强连通分量。Tarjan算法也可以用于求解LCA问题,在有向无环图(DAG)中。 具体来说,在使用Tarjan算法求解LCA时,我们需要进行两次DFS遍历。首先,我们从根节点开始,遍历每个节点,并记录每个节点的深度(即从根节点到该节点的路径长度)。然后,我们再进行一次DFS遍历,但这次我们在遍历的过程中,同时进行LCA的查找。对于每个查询,我们将两个待查询节点放入一个查询列表中,并在遍历过程中记录每个节点的祖先节点。 在遍历的过程中,我们会遇到以下几种情况: 1. 如果当前节点已被访问过,说明已经找到了该节点的祖先节点,我们可以更新该节点及其所有后代节点的祖先节点。 2. 如果当前节点未被访问过,我们将其标记为已访问,并将其加入到查询列表中。 3. 如果当前节点有子节点,我们继续递归遍历子节点。 最终,对于每个查询,我们可以通过查询列表中的两个节点的最近公共祖先节点来求解LCA。 需要注意的是,Tarjan算法的时间复杂度为O(V+E),其中V为节点数,E为边数。因此,对于大规模的树结构,Tarjan算法是一种高效的求解LCA问题的方法。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值