zoukankan      html  css  js  c++  java
  • Tarjan算法 求 有向图的强连通分量

    百度百科 

    https://baike.baidu.com/item/tarjan%E7%AE%97%E6%B3%95/10687825?fr=aladdin

    参考博文

    http://blog.csdn.net/qq_34374664/article/details/77488976

    http://blog.csdn.net/mengxiang000000/article/details/51672725

    https://www.cnblogs.com/shadowland/p/5872257.html

    算法介绍(基于DFS)

    了解tarjan算法之前你需要知道:强连通,强连通图,强连通分量。

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

    在一个有向图G里,设两个点a和b, 发现由a有一条路可以走到b,由b又有一条 路可以走到a,我们就叫这两个顶点(a,b)强连通。(注意间接连接也可以)

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

    强连通分量:在一个有向图G中,有一个子图G2,这个子图G2每2个点都满足强连通,我们就 把这个子图G2叫做强连通分量 。

    我们来看一个有向图方便理解:

     

    标注蓝色线条框框的部分就是一个强连通分量,节点3也是一个强连通分量

    我们再来看一个图(百度百科中的图):

    子图{1,2,3,4}为一个强连通分量,因为顶点1,2,3,4两两可达。{5},{6}也分别是两个强连通分量。

     

     

    Tarjan算法是基于对图深度优先搜索的算法,每个强连通分量实际上为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入到一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。

     

    在Tarjan算法中,有如下定义。

    (注意,下面的定义如看不明白没关系,多看后面的模拟就明白了)

    DFN[u]数组 : 在DFS中该节点被搜索的次序编号,每个点的次序编号都不一样。

    通俗地解释DFN[u]: 意思就是在DFS的过程中,当前的这个节点是第几个被遍历到的点

    LOW[u]数组 : u或u的子树能够追溯到的最早的栈中节点的次序号

    通俗地解释LOW[u]: 就是在DFS的过程中,如果当前节点是极大强联通子图的话,他的根节点的标号就是对应的LOW值:

     

    当DFN[ u ]==LOW[ u ]时,u或u的子树可以构成一个强连通分量。

    通俗地解释:当DFN[ u ]==LOW[ u ]时,以u为根的搜索子树上所有节点是一个强连通分量。

     

    回溯条件: DFS遇到的节点在已在栈中或者DFS遇到无出度的节点时就回溯。

    回溯时需要维护LOW[u]的值。

    如果下一个要遍历的节点在栈中,那么就要把当前节点的LOW[u]值设置成下一个节点(在栈中)的DFN[v]值。如:LOW[u]=DFN[v]  或者LOW[u]= min(LOW[u], DFN[v])

    如果还需要接着回溯,那么接着维护LOW[u]=min(LOW[u],LOW[v])

    (即使v搜过了也要进行这步操作,但是v一定要在栈内才行)

    u代表当前节点,v代表其能到达的节点。在进行一层深搜之后回溯的时候,维护LOW[u]的值。如果我们发现了某个节点回溯之后维护的LOW[u]值还是==DFN[u]的值,那么这个节点无疑就是一个关键节点:

     

    算法演示:以1为Tarjan 算法的起始点,如图:前面不明白没关系,重点从这里开始看

     

     

    假如从1号节点开始遍历,开始dfs,并不断设置当前节点的DFN值和LOW值,并把当前这个节点压入栈中,那么第一次在节点6处停止,因为6没有出度。

    那么此时的DFN和LOW值分别为:

    从1开始:   DFN[1]=LOW[1]= ++index ----1

    入栈 1

    由1进入3: DFN[3]=LOW[3]= ++index ----2

    入栈 1  3

    由3进入5: DFN[5]=LOW[5]= ++index ----3

    入栈 1  3  5

    由5进入6: DFN[6]=LOW[6]= ++index ----4

    入栈 1  3  5  6                

    可以用下图来表示:

       

    因为节点6无出度,于是判断 DFN[6]==LOW[6],把6出栈(pop)。

    {6}是一个强连通分量。

    目前栈的节点有: 1  3  5  见下图:

     

    之后回溯到节点5,节点6被访问过了并出栈(pop)了,所以它也没有能访问的边了,

    那么 DFN[5]==LOW[5],{5} 也是一个强连通分量,弹出5

    目前栈的节点有: 1  3

     

    返回节点3,继续搜索到节点4,节点4是新节点,设DFN[4]=LOW[4]=5并把4加入堆栈。

    DFN[4]=LOW[4]= ++index -----5

    入栈 1  3   4  见下图:

     

    继续节点4往下找,找到了节点1 。

    因为1号节点还在栈中,那么就代表着栈中的现有的所有元素构成了一个强连通图

    (仔细想想、、兜了一圈又回到起点1)

     

    回溯到节点4,更新 LOW[4]的值: LOW[4]= min(LOW[4], DFN[1])   值更新为1

    再接着访问4的下一个节点6,节点6 被访问过并POP了,就不用管它了。

    再回溯到节点3,更新 LOW[3]的值: LOW[3]= min(LOW[3], LOW[4])   值更新为1

    3号节点也没有能访问的下一个节点了。图如下: 

     

    再回溯到节点1,更新 LOW[1]的值: LOW[1]= min(LOW[1], LOW[3])   值还是为1

    节点1还有边没有走过。发现节点2,访问节点2,节点2是新节点,放入

    DFN[2]=LOW[2]=++index ----6

    入栈 1  3  4  2 

    由节点2,走到4,发现4被访问过了,4还在栈里,

    回溯到节点2 更新LOW[2] = min(LOW[2], DFN[4])     LOW[2]=5

    节点2也没有可访问的下一个节点了。

    再回溯到节点1 更新LOW[1] = min(LOW[1], LOW[2])     LOW[1]=1

    这时我们发现LOW[1]==DFN[1]   说明以1为 根节点 的强连通分量已经找完了。

    将栈中1,3,4,2全部节点都出栈{1,3,4,2} 是强连通分量。图如下

                

    至此,算法结束。经过该算法,求出了图中全部的三个强连通分量{1,3,4,2},  {5},  {6}

    可以发现,运行Tarjan算法的过程中,每个顶点都被访问了一次,且只进出了一次堆栈,每条边也只被访问了一次,所以该算法的时间复杂度为O(N+M)

    实战:(后面有Tarjan算法的伪代码及模板,可以参考)

    P1726 上白泽慧音   https://www.luogu.org/problemnew/show/1726

    P2661 信息传递     https://www.luogu.org/problemnew/show/2661

    P3379 LCA  Tarjan算法  https://www.luogu.org/problemnew/show/3379

    P1262 间谍网络 (提示:可用Tanjan缩点) https://www.luogu.org/problemnew/show/1262

    P3387 【模板】缩点   https://www.luogu.org/problemnew/show/3387 

    以下4道题是北京大学的是英文题,如不明白意思可看下面的翻译:

    题解 http://blog.csdn.net/u012469987/article/details/51292558#poj-1236-network-of-schools 

    http://poj.org/problem?id=2186     http://poj.org/problem?id=1236

    http://poj.org/problem?id=1904      http://poj.org/problem?id=1330 

     

     

    接下来我们讨论一下Tarjan算法另外能够干一些什么:

    既然我们知道,Tarjan算法相当于在一个有向图中找有向环,那么我们Tarjan算法最直接的能力就是缩点辣!缩点基于一种染色实现,我们在Dfs的过程中,尝试把属于同一个强连通分量的点都染成一个颜色,那么同一个颜色的点,就相当于一个点。

    比如刚才的实例图中缩点之后就可以变成这样: 

    将一个有向带环图变成了一个有向无环图(DAG图)。很多算法要基于有向无环图才能进行的算法就需要使用Tarjan算法实现染色缩点,建一个DAG图然后再进行算法处理。在这种场合,Tarjan算法就有了很大的用武之地!

    那么这个时候 ,我们再引入一个数组color【i】表示节点i的颜色,再引入一个数组stack【】实现一个栈,然后在Dfs过程中每一次遇到点都将点入栈,在每一次遇到关键点的时候将栈内元素弹出,一直弹到栈顶元素是关键点的时候为止,对这些弹出来的元素进行染色即可。

    缩点代码实现: 

    void Tarjan(int u)  //此代码仅供参考
    {
        vis[u]=1;
        low[u]=dfn[u]=cnt++;
        stack[++tt]=u;
        for(int i=0;i<mp[u].size();i++)
        {
            int v=mp[u][i];
            if(vis[v]==0)Tarjan(v);
            if(vis[v]==1)low[u]=min(low[u],low[v]);
        }
        if(dfn[u]==low[u])
        {
            sig++;
            do
            {
                low[stack[tt]]=sig;
                color[stack[tt]]=sig;
                vis[stack[tt]]=-1;
            }
            while(stack[tt--]!=u);
        }
    }

    Tarjan算法伪代码参考:

    //注意,v指的是u能达到的下一个节点
    tarjan(u){
      DFN[u]=Low[u]=++Index   // 为节点u设定次序编号和Low初值
      Stack.push(u)           // 将节点u压入栈中
      for each (u, v) in E     // 枚举每一条边
        if (v is not visted) // 如果节点v未被访问过
            tarjan(v) // 继续向下找
            Low[u] = min(Low[u], Low[v])
        else if (v in S) // 如果节点u还在栈内
            Low[u] = min(Low[u], DFN[v])
      if (DFN[u] == Low[u]) // 如果节点u是强连通分量的根
      repeat v = S.pop  // 将v退栈,为该强连通分量中一个顶点
      print  v
      until (u== v)
    }

    Tanjan算法模板:

    void Tarjan ( int x )
     {
      dfn[ x ] = ++dfs_num ;
      low[ x ] = dfs_num ;
      vis [ x ] = true ; //是否在栈中
      stack [ ++top ] = x ;
      for ( int i=head[ x ] ; i!=0 ; i=e[i].next )
          {
              int temp = e[ i ].to ;
              if ( !dfn[ temp ] )
               {
                 Tarjan ( temp ) ;
                  low[ x ] = gmin ( low[ x ] , low[ temp ] ) ;
               }
                else if ( vis[ temp ])low[ x ] = gmin ( low[ x ] , dfn[ temp ] ) ;
          }
          if ( dfn[ x ]==low[ x ] )            //构成强连通分量
          {
                vis[ x ] = false ;
                 color[ x ] = ++col_num ;    //染色
                 while ( stack[ top ] != x )  //清空
                 {
                  color [stack[ top ]] = col_num ;
                 vis [ stack[ top-- ] ] = false ;
                 }
                     top -- ;
           }
    }

    Tanjan算法另一个模板:

    #define M 5010       //题目中可能的最大点数
    int STACK[M],top=0;  //Tarjan算法中的栈
    bool InStack[M]; //检查是否在栈中
    int DFN[M];    //深度优先搜索访问次序
     
    int Low[M];  //能追溯到的最早的次序
    int ComponentNumber=0;  //有向图强连通分量个数
    int Index=0;  //索引号
    vector<int> Edge[M];  //邻接表表示
    vector<int> Component[M];  //获得强连通分量结果
    int InComponent[M];  //记录每个点在第几号强连通分量里
    int ComponentDegree[M]; //记录每个强连通分量的度
     
    void Tarjan(int i)
    {
        int j;
        DFN[i]=Low[i]=Index++;
        InStack[i]=true;STACK[++top]=i;
        for (int e=0;e<Edge[i].size();e++)
        {
            j=Edge[i][e];
            if (DFN[j]==-1)
            {
                Tarjan(j);
                Low[i]=min(Low[i],Low[j]);
            }
            else
                if (InStack[j]) Low[i]=min(Low[i],DFN[j]);
        }
        if (DFN[i]==Low[i])
        {
            ComponentNumber++;
            do{
                j=STACK[top--];
                InStack[j]=false;
                Component[ComponentNumber].
                push_back(j);
                InComponent[j]=ComponentNumber;
            }
            while (j!=i);
        }
    }

    Tarjan算法裸代码:

    输入:

    一个图有向图。

    输出:

    它每个强连通分量。

    这个图就是刚才讲的那个图。一模一样。

    input:

    6 8

    1 3

    1 2

    2 4

    3 4

    3 5

    4 6

    4 1

    5 6

    output:

    6

    5

    3 4 2 1 

    #include<cstdio>
     #include<algorithm>
     #include<string.h>
     using namespace std;
     struct node
    {
         int v,next;
     }edge[1001];
     int DFN[1001],LOW[1001];
     int stack[1001],heads[1001],visit[1001],cnt,tot,index;
    void add(int x,int y)
    {
         edge[++cnt].next=heads[x];
         edge[cnt].v = y;
         heads[x]=cnt;
        return ;
     }
     void tarjan(int x)  //代表第几个点在处理。递归的是点。
     {
         DFN[x]=LOW[x]=++tot; //新进点的初始化。
         stack[++index]=x; //进栈
         visit[x]=1;  //表示在栈里
        for(int i=heads[x];i!=-1;i=edge[i].next)
         {
             if(!DFN[edge[i].v]) //如果没访问过
    {   
                tarjan(edge[i].v);  //往下进行延伸,开始递归
                 LOW[x]=min(LOW[x],LOW[edge[i].v]);//递归出来,比较谁是谁的儿子/父亲,就是树的对应关系,涉及到强连通分量子树最小根的事情。
               }
            else if(visit[edge[i].v ])//如果访问过,并且还在栈里
    {  
                 LOW[x]=min(LOW[x],DFN[edge[i].v]);//比较谁是谁的儿子/父亲。就是链接对应关系
              }
         }
         if(LOW[x]==DFN[x]) //发现是整个强连通分量子树里的最小根。
        {
             do{
                printf("%d ",stack[index]);
                 visit[stack[index]]=0;
                 index--;
             }while(x!=stack[index+1]);//出栈,并且输出。
             printf("
    ");
         }
         return ;
     }
     int main()
     {
         memset(heads,-1,sizeof(heads));
         int n,m;
         scanf("%d%d",&n,&m);
        int x,y;
         for(int i=1;i<=m;i++)
         {
             scanf("%d%d",&x,&y);
            add(x,y);
         }
        for(int i=1;i<=n;i++)
             if(!DFN[i])  tarjan(i);//当这个点没有访问过,就从此点开始。防止图没走完
        return 0;
     }
  • 相关阅读:
    go语言学习笔记四(函数、包和错误处理)
    objection内存漫游实战
    脱壳工具FRIDA-DEXDump
    jsdom 用法技巧
    关于adb安装指定版本
    ob混淆
    js原型链hook
    js逆向核心:扣代码2
    ssl_logger捕获得物app双向验证数据
    js逆向核心:扣代码
  • 原文地址:https://www.cnblogs.com/wozaixuexi/p/8321602.html
Copyright © 2011-2022 走看看