• Tarjan算法浅谈



    算法1:Tarjan求强连通分量

    这个算是Tarjan中最基础的算法了 (反正我的第一个就是这个QAQ)。主要用途为判环和缩点,在很多图论题中都可以使用。

    前置芝士:

    强连通:如果两个顶点可以相互通达,则称两个顶点 强连通。

    强连通图:如果有向图(G)的每两个顶点都 强连通,称(G)是一个强连通图。

    强连通分量:非强连通图有向图的极大强连通子图,称为强连通分量。


    然后我们就可以开始学习Tarjan算法啦!

    Tarjan算法基于DFS (相信我应该不用再解释)和数据结构栈,通过搜索每一个子树来维护两个数组(dfn[])(low[])(下面解释),从而查找强连通分量。

    现在我们引入两个在Tarjan算法中非常重要的两个数组:

    dfn[u]:节点(u)搜索的次序编号(时间戳)

    low[u]:(u)(u)的子树能够追溯到的最早的栈中节点的次序号。

    Tarjan算法的过程就是,每次将DFS到的点加入栈中,然后判断出边到达的节点是否在栈中,如果是,则(low[u]=min(low[u],dfn[v]))。而当自己这棵子树跑完了之后,发现(dfn[u]==low[u]),就说明(u)的子节点到达不了时间戳比(u)更早的点,所以(u)的子树中已经成环,所以打上标记。(注意:每次DFS到一个节点时,需要将(low[])值附成(dfn[])的值)

    我们来模拟一下过程

    首先DFS到(1),所以(dfn[1]=low[1]=1),并把(1)加入栈中。

    栈中:(1)

    然后DFS到(2),(dfn[2]=low[2]=2),并把(2)加入栈中。

    栈中:(1,2)

    DFS到(5),(dfn[5]=low[5]=3),并把(5)加入栈中。

    栈中:(1,2,5)

    DFS到(7),(dfn[7]=low[7]=4),并把(7)加入栈中。

    栈中:(1,2,5,7)

    这时回溯时发现(dfn[7]==low[7]),所以把栈顶到(7)归为一个强连通分量,即{(7)}本身是一个强连通分量。

    继续DFS到(4),(dfn[4]=low[4]=5),并把(4)加入栈中。

    栈中:(1,2,5,4)

    下一条边指向(1),发现已经在栈中,于是把(low[4]=min(low[4],dfn[1])),因此(low[4]=1)

    ......

    以此类推,最后栈中为{(1,2,5,4,3)},发现(dfn[1]==low[1]),故将栈顶至(1)归为一个强连通分量,说明{(1,2,5,4,3)}互相连通。

    至此算法结束

    模板代码如下:(可以结合代码理解)

    #include<bits/stdc++.h>
    #define re register
    using namespace std;
    const int N=10005,M=50005;
    inline int read()
    {
    	re int x=0,f=1;
    	re char ch=getchar();
    	for(;ch>'9'||ch<'0';ch=getchar())if(ch=='-')f*=-1;
    	for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<1)+(x<<3)+(ch^48);
    	return x*f;
    }
    struct edge{int v,net;}e[M];
    int ans,n,m,tim,cnt,hd[N],dfn[N],low[N],st[N],top,col,color[N],num[N];
    bool yor[N],vis[N];
    inline void add(int v,int u){e[++cnt].v=v,e[cnt].net=hd[u],hd[u]=cnt;}
    void Tarjan(int u)
    {
    	yor[u]=vis[u]=1;//yor[]说明已经到达过该节点,vis[]说明已经入栈
    	dfn[u]=low[u]=++tim;//更新时间戳
    	st[++top]=u;//入栈
    	for(re int v,i=hd[u];i;i=e[i].net)
    	{
    		v=e[i].v;
    		if(dfn[v]==0)
    		{
    			Tarjan(v);//继续DFS
    			low[u]=min(low[u],low[v]);//可能子节点能达到更早的节点
    		}
    		else if(vis[v])
    			low[u]=min(low[u],dfn[v]);//更新low[u]
    	}
    	if(dfn[u]==low[u])
    	{
    		++col;
    		for(;st[top]!=u;color[st[top]]=col,vis[st[top--]]=0);//将栈顶到u归为一个强连通分量
    		color[st[top]]=col,vis[st[top--]]=0;//弹出栈顶
    	}
    }
    int main()
    {
    	scanf("%d%d",&n,&m);
    	for(re int i=1;i<=m;i++)add(read(),read());
    	for(re int i=1;i<=n;i++)
    		if(yor[i]==0)Tarjan(i);//可能整个图不是连通图,要每个点都判断
    	return 0;
    }
    

    算法2:Tarjan求最近公共祖先(LCA)

    相信大家应该都已经学过倍增求LCA,是一个在线算法,(O(n))预处理后每次询问时间复杂度为(O(log^2n))。我们今天来谈谈一个离线求LCA的方法:Tarjan算法。

    如这张图:

    假如我们的询问为:

    ((5,7),(4,8),(8,10),(9,1),(10,11))

    首先我们把询问复制一遍:

    ((5,7),(7,5),(4,8),(8,4),(8,10),(10,8),(9,1),(1,9),(10,11),(11,10))

    至于为什么要复制一遍等后面再讲。

    然后我们把与每个点相关的询问加入一个链表中:

    如:与(8)有关的询问有((8,4),(8,10))(8)的链表就为(4->10)

    然后就可以开始DFS了:

    首先从(1)开始,发现与(1)有关的询问有((1,10)),但(10)没有访问到,丢一边不管,继续

    直到如上图,和(9)有关的询问有((9,1)),而这时候(1)已经被访问过了,我们就找绿色节点(被访问过且未回溯的点),这个点要是(1)的最大深度的祖先节点(可以是自己),这个节点就是它们的LCA,很明显就是(1)

    咱们继续,又访问到节点(4),发现也有与之相关的询问((4,8)),于是找节点(8)的深度最大的绿色祖先节点,发现是(2),因此(2)就是((4,8))的LCA。

    ......

    然后我们就可以离线求出所有的询问答案了!时间复杂度为(O(n+q))

    但有一个问题,怎么找出节点(8)的深度最大的绿色祖先节点?我们可以在回溯的时候设一个数组(f[8]=3),然后(3)回溯时(f[3]=2),这样就可以通过并查集的方式(O(n)),处理完所有的节点了!QAQ

    至此算法结束

    代码就不放了,留给大家自己思考 (其实就是懒


    算法三:Tarjan求割点和桥:咕咕咕~~

    累了,下次再来填这个坑吧。。。

  • 相关阅读:
    获取label标签内for的属性值-js
    js万亿级数字转汉字的封装方法
    a标签的伪元素的应用——link,hover,visited,active
    JS声明变量的写法
    VUE环境配置步骤及相关Git Bash命令的使用
    display:inline-block下,元素不能在同一水平线及元素间无margin间距的问题解决方法
    css变量的用法——(--cssName)
    img的属性alt 与 title的区别
    返回结果的HTTP状态码——《图解http》第四章
    HTTP报文内部的HTTP信息——《图解HTTP》第三章
  • 原文地址:https://www.cnblogs.com/jkzcr01-QAQ/p/13575207.html
Copyright © 2020-2023  润新知