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一棵树后,
也就变成了一个十分常规的问题了,然后就可以再运用合理的数据结构或算法将其迎刃而解!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值