有向图强连通分量的Tarjan算法

我就是我 2022-08-02 05:54 238阅读 0赞

[有向图强连通分量]

在有向图G中,如果两个顶点间至少存在一条路径,称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。非强连通图有向图的极大强连通子图,称为强连通分量(strongly connected components)。

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

image

直接根据定义,用双向遍历取交集的方法求强连通分量,时间复杂度为O(N^2+M)。更好的方法是Kosaraju算法或Tarjan算法,两者的时间复杂度都是O(N+M)。本文介绍的是Tarjan算法。

[Tarjan算法]

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

定义DFN(u)为节点u搜索的次序编号(时间戳)(记录节点u第一次被访问时的步数 ),Low(u)为u或u的子树能够追溯到的最早的栈中节点的次序号。由定义可以得出,

  1. Low(u)=Min
  2. {
  3. DFN(u),
  4. Low(v),(u,v)为树枝边,uv的父节点
  5. DFN(v),(u,v)为指向栈中节点的后向边(非横叉边)
  6. }

当DFN(u)=Low(u)时,以u为根的搜索子树上所有节点是一个强连通分量。

算法伪代码如下

  1. tarjan(u)
  2. {
  3. DFN[u]=Low[u]=++Index // 为节点u设定次序编号和Low初值
  4. Stack.push(u) // 将节点u压入栈中
  5. for each (u, v) in E // 枚举每一条边
  6. if (v is not visted) // 如果节点v未被访问过
  7. tarjan(v) // 继续向下找
  8. Low[u] = min(Low[u], Low[v])
  9. else if (v in S) // 如果节点v还在栈内
  10. Low[u] = min(Low[u], DFN[v])
  11. if (DFN[u] == Low[u]) // 如果节点u是强连通分量的根
  12. repeat
  13. v = S.pop // 将v退栈,为该强连通分量中一个顶点
  14. print v
  15. until (u== v)
  16. }

接下来是对算法流程的演示。

从节点1开始DFS,把遍历到的节点加入栈中。搜索到节点u=6时,DFN[6]=LOW[6],找到了一个强连通分量。退栈到u=v为止,{6}为一个强连通分量。

image

返回节点5,发现DFN[5]=LOW[5],退栈后{5}为一个强连通分量。

image

返回节点3,继续搜索到节点4,把4加入堆栈。发现节点4向节点1有后向边,节点1还在栈中,所以LOW[4]=1。节点6已经出栈,(4,6)是横叉边,返回3,(3,4)为树枝边,所以LOW[3]=LOW[4]=1。

image

继续回到节点1,最后访问节点2。访问边(2,4),4还在栈中,所以LOW[2]=DFN[4]=5。返回1后,发现DFN[1]=LOW[1],把栈中节点全部取出,组成一个连通分量{1,3,4,2}。

image

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

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

求有向图的强连通分量还有一个强有力的算法,为Kosaraju算法。Kosaraju是基于对有向图及其逆图两次DFS的方法,其时间复杂度也是O(N+M)。与Trajan算法相比,Kosaraju算法可能会稍微更直观一些。但是Tarjan只用对原图进行一次DFS,不用建立逆图,更简洁。在实际的测试中,Tarjan算法的运行效率也比Kosaraju算法高30%左右。此外,该Tarjan算法与求无向图的双连通分量(割点、桥)的Tarjan算法也有着很深的联系。学习该Tarjan算法,也有助于深入理解求双连通分量的Tarjan算法,两者可以类比、组合理解。

求有向图的强连通分量的Tarjan算法是以其发明者Robert Tarjan命名的。Robert Tarjan还发明了求双连通分量的Tarjan算法,以及求最近公共祖先的离线Tarjan算法,在此对Tarjan表示崇高的敬意。

附:tarjan算法的C++程序

  1. void tarjan(int i)
  2. {
  3. int j;
  4. DFN[i]=LOW[i]=++Dindex;
  5. instack[i]=true;
  6. Stap[++Stop]=i;
  7. for (edge *e=V[i];e;e=e->next)
  8. {
  9. j=e->t;
  10. if (!DFN[j])
  11. {
  12. tarjan(j);
  13. if (LOW[j]<LOW[i])
  14. LOW[i]=LOW[j];
  15. }
  16. else if (instack[j] && DFN[j]<LOW[i])
  17. LOW[i]=DFN[j];
  18. }
  19. if (DFN[i]==LOW[i])
  20. {
  21. Bcnt++;
  22. do
  23. {
  24. j=Stap[Stop--];
  25. instack[j]=false;
  26. Belong[j]=Bcnt;
  27. }
  28. while (j!=i);
  29. }
  30. }
  31. void solve()
  32. {
  33. int i;
  34. Stop=Bcnt=Dindex=0;
  35. memset(DFN,0,sizeof(DFN));
  36. for (i=1;i<=N;i++)
  37. if (!DFN[i])
  38. tarjan(i);
  39. }

附模板:

  1. #include<iostream>
  2. #include<vector>
  3. using namespace std;
  4. const int MAX=10001;
  5. int Stop;//栈中的元素个数
  6. int cnt;//记录连通分量的个数
  7. int visitNum;//记录遍历的步数
  8. int DFN[MAX]; //记录节点u第一次被访问时的步数
  9. int LOW[MAX]; //记录与节点u和u的子树节点中最早的步数
  10. bool instack[MAX];//记录节点u是否在栈中
  11. int Stap[MAX];//栈
  12. int Belong[MAX];//记录每个节点属于的强连通分量编号
  13. int N;//节点个数
  14. vector<int> tree[MAX];
  15. void tarjan(int i)
  16. {
  17. int j;
  18. DFN[i]=LOW[i]=++visitNum;
  19. instack[i]=true;
  20. Stap[++Stop]=i;//将当前节点压入栈中
  21. for (unsigned k=0;k<tree[i].size();k++)
  22. {
  23. j=tree[i][k];
  24. if (!DFN[j]) //j还没有被访问过
  25. {
  26. tarjan(j);
  27. //父节点是子节点的子节点
  28. if (LOW[j]<LOW[i])
  29. LOW[i]=LOW[j];
  30. }
  31. //与j相连,但是j已经被访问过,且还在栈中
  32. //用子树节点更新节点第一次出现的时间
  33. else if (instack[j] && DFN[j]<LOW[i])
  34. LOW[i]=DFN[j];
  35. }
  36. //节点i是强连通分量的根
  37. if (DFN[i]==LOW[i])
  38. {
  39. cnt++;
  40. //输出找到的强连通分量
  41. cout<<"连通分量"<<cnt<<": ";
  42. //退栈,直至找到根为止
  43. do
  44. {
  45. j=Stap[Stop--];
  46. instack[j]=false;
  47. cout<<j<<" ";
  48. Belong[j]=cnt;
  49. }
  50. while (j!=i);
  51. cout<<endl;
  52. }
  53. }
  54. void solve()
  55. {
  56. Stop=cnt=visitNum=0;
  57. memset(DFN,0,sizeof(DFN));
  58. for (int i=1;i<=N;i++)
  59. if (!DFN[i])//有可能图不是连通图
  60. tarjan(i);
  61. }
  62. int main()
  63. {
  64. N=6;
  65. tree[1].push_back(3);
  66. tree[1].push_back(2);
  67. tree[2].push_back(4);
  68. tree[3].push_back(5);
  69. tree[3].push_back(4);
  70. tree[4].push_back(1);
  71. tree[4].push_back(6);
  72. tree[5].push_back(6);
  73. solve();
  74. for(int i=1;i<=N;i++)
  75. cout<<Belong[i]<<" ";
  76. cout<<endl;
  77. return 0;
  78. }

发表评论

表情:
评论列表 (有 0 条评论,238人围观)

还没有评论,来说两句吧...

相关阅读

    相关 连通分量标记算法

    引言 图像分割领域中一个重要步骤是求取图像的连通区域,后续图像轮廓理解的基石。 Matlab图像工具箱函数bwlabel函数,就是对二值图像连通区域进行标记的函数。