【图论】Tarjan算法详解

浅浅的花香味﹌ 2023-07-25 13:00 82阅读 0赞

在学习Tarjan算法之前,要先了解强连通的相关知识点!

  • 强连通: 在一个有向图G里,如果有两个点(a、b)可以相互到达,我们就叫这两个顶点(a,b)为强连通。
  • 强连通图: 如果在一个有向图G中,每两个点都强连通(可以相互到达),我们就叫这个图为强连通图。
  • 强连通分量(SCC): 在一个有向图G中,有一个子图,这个子图每2个点都满足强连通,我们就叫这个子图叫做 强连通分量,孤立的点也是一个强连通分量。

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

在这里插入图片描述

Tarjan算法

Tarjan算法是一种用来求解有向图强连通分量的线性时间的算法。可以找强连通分量,也可以找缩点、割点等。

算法思路:

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

为了使这颗搜索树在遇到强连通分量的节点的时候能顺利进行。在DFS时每个点都需要记录两个数组:

  • DFN[u]: 代表u点DFS到的时间,即时间戳,简单来说就是 第几个被搜索到的。可知在同一个DFS树的子树中,DFN[u] 越小,则其越浅。
  • Low[u]: 代表在DFS树中,u或u的子树 能够追溯到的最早的栈中节点的 时间戳。

算法过程:

(1)数组的初始化:对图进行深度优先搜索(DFS),在搜索过程中用 DFN 记录搜索的顺序。当首次搜索到点u 时,DFN 与 Low 数组的值都为 到该点的时间。

(2)堆栈:采用栈(记录已经搜索过的但是未删除的点),每搜索到一个点,将它压入栈顶。如果这个点有 出度 就继续往下找,直到找到底。

(3)每次返回时都将子节点与该节点的Low值进行比较,谁小就取谁,保证最小的子树根。

  • 当点u 有与点u’ 相连时,如果此时(时间为DFN[u]时)u’不在栈中,u的 Low 值为两点的 Low值 中较小的一个。
  • 当点u 有与点u’ 相连时,如果此时(时间为DFN[u]时)u’在栈中,u的 Low 值为 u的 Low值 和u’的 DFN值 中较小的一个

(4)每当搜索到一个点经过以上操作后(也就是子树已经全部遍历)的 Low值等于DFN值( DFN[u] == Low[u]),则将它以及在它之上的元素弹出栈。这些出栈的元素组成一个强连通分量。

原因:u点在DFS树中,子节点(后代)不能找到更浅的点,那么 u点及其后代构成一个SCC(强连通分量)。且 u点 是这个强连通分量的根节点(因为这个Low[] 值是这个强连通分量里最小的。)

(5)继续搜索(或许会更换搜索的起点,因为整个有向图可能分为两个不连通的部分),直到所有点被遍历。

在这里插入图片描述

伪代码:

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

流程演示

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

  • 至此,算法结束。经过该算法,求出了图中全部的三个强连通分量{1,3,4,2},{5},{6}。
  • 可以发现,运行Tarjan算法的过程中,每个顶点都被访问了一次,且只进出了一次堆栈,每条边也只被访问了一次,所以该算法的时间复杂度为O(V+E)。

还是没能理解?那就再来一个例子演示

  • 首先a、b、c、d依次入栈:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  • 直到e不能再往下搜索,此时判断 dfn[4] == low[4],因此e是一个强连通分量,并从栈中弹出。

    在这里插入图片描述

  • 然后节点d入栈:

在这里插入图片描述

  • 接下来 节点d 指向节点b,因此把节点d的 low值更新为2。

    在这里插入图片描述

  • 然后开始回溯,发现节点d:(5,2)不等,节点c:(3,2)不等,直到发现节点b:(2,2)相等,说明以b为根的子树就是一个强连通分量,同时在栈中弹出。

在这里插入图片描述

  • 接下来已经回溯到节点a,然后指向节点f,于是f入栈:

在这里插入图片描述

  • 依次节点g入栈:

在这里插入图片描述

  • 因为节点g 指向节点a,所以把节点g 的low值更新为1。

在这里插入图片描述

  • 然后开始回溯,由节点g 回溯到节点f,把f的low值也更新为1,再回溯到a,就得到了最后一个强连通分量。

在这里插入图片描述

Code:

  1. import java.util.ArrayList;
  2. import java.util.Arrays;
  3. import java.util.List;
  4. import java.util.Stack;
  5. public class Tarjan {
  6. private int numOfNode; //节点数
  7. private List< ArrayList<Integer> > graph; //图
  8. private List< ArrayList<Integer> > result; //保存极大强连通图
  9. private boolean[] inStack; //表示节点是否在栈内,因为在stack中寻找一个节点不方便。这种方式查找快
  10. private Stack<Integer> stack;
  11. private int[] dfn;
  12. private int[] low;
  13. private int time; //time表示在栈中的编号
  14. public Tarjan(List< ArrayList<Integer> > graph, int numOfNode){ //带参构造
  15. this.graph = graph;
  16. this.numOfNode = numOfNode;
  17. this.inStack = new boolean[numOfNode];
  18. this.stack = new Stack<Integer>(); //栈中元素都为整数
  19. dfn = new int[numOfNode];
  20. low = new int[numOfNode];
  21. Arrays.fill(dfn, -1); //将dfn所有元素都置为-1,其中dfn[i]=-1代表节点i还有没被访问过
  22. Arrays.fill(low, -1);
  23. result = new ArrayList<ArrayList<Integer>>();
  24. }
  25. public List< ArrayList<Integer> > run(){
  26. for(int i=0;i<numOfNode;i++){
  27. if(dfn[i] == -1){
  28. tarjan(i);
  29. }
  30. }
  31. return result;
  32. }
  33. //算法的主要代码
  34. public void tarjan(int current){ //代表第几个点在处理,递归的是点
  35. dfn[current] = low[current] = time++; //新的点首先初始化
  36. inStack[current] = true; //表示在栈里
  37. stack.push(current); //进栈
  38. for(int i=0; i<graph.get(current).size(); i++){ //搜索相连节点
  39. int next = graph.get(current).get(i);
  40. if(dfn[next] == -1){ //如果没被访问过(-1代表没有被访问)
  41. tarjan(next); //递归调用
  42. low[current]=Math.min(low[current], low[next]); //更新所能到的上层节点(涉及到强连通分量子树最小根的事情)
  43. }else if(inStack[next]){ //如果在栈中,并且被访问过
  44. low[current]=Math.min(low[current], dfn[next]); //到栈中最上端的节点
  45. }
  46. }
  47. if(low[current] == dfn[current]){ //发现是整个强连通分量子树里的最小根
  48. ArrayList<Integer> temp = new ArrayList<Integer>();
  49. int j = -1;
  50. while(current!=j){
  51. j = stack.pop(); //出栈,并且输出
  52. inStack[j] = false; //修改状态为不在栈中
  53. temp.add(j);
  54. }
  55. result.add(temp);
  56. }
  57. }
  58. public static void main(String[] args) {
  59. //创建图
  60. int numOfNode = 6;
  61. List< ArrayList<Integer> > graph = new ArrayList<ArrayList<Integer>>();
  62. for(int i=0; i<numOfNode; i++){
  63. graph.add(new ArrayList<Integer>());
  64. }
  65. graph.get(0).add(1);
  66. graph.get(0).add(2);
  67. graph.get(1).add(3);
  68. graph.get(2).add(3);
  69. graph.get(2).add(4);
  70. graph.get(3).add(0);
  71. graph.get(3).add(5);
  72. graph.get(4).add(5);
  73. //调用Tarjan算法求极大连通子图
  74. Tarjan t = new Tarjan(graph, numOfNode);
  75. List< ArrayList<Integer> > result = t.run(); //结果代码
  76. //打印结果
  77. for(int i=0; i<result.size(); i++){
  78. for(int j=0; j<result.get(i).size(); j++){
  79. System.out.print(result.get(i).get(j) + " ");
  80. }
  81. System.out.println();
  82. }
  83. }
  84. }

例题

原题链接:http://acm.hdu.edu.cn/showproblem.php?pid=1269

题意: 一个有向图,有 n 个点(n <= 10000)和 m 条边(m <= 100000)。判断整个图是否强连通,如果是,输出 “Yes”,否则,输出“No”。

思路: Kosaraju 算法模板题,直接求整个图是否为强连通分量。

Code:

  1. #include <iostream>
  2. #include <vector>
  3. #include <cstring>
  4. using namespace std;
  5. const int N = 100010;dfn
  6. vector<int> G[N];
  7. int dfn[N],low[N],sccno[N],s[N];
  8. int cnt,dfn,top;
  9. void dfs(int u) {
  10. s[top++] = u;
  11. dfn[u] = low[u] = ++dfn;
  12. for(int i=0; i<G[u].size(); i++) {
  13. int v=G[u][i];
  14. if(!dfn[v]){
  15. dfs(v);
  16. low[u] = min(low[u],low[v]);
  17. }
  18. else if(!sccno[v])
  19. low[u] = min(low[u],dfn[v]);
  20. }
  21. if(low[u]==dfn[u]){
  22. cnt++;
  23. while(1){
  24. int v = s[--top];
  25. sccno[v] = cnt;
  26. if(u==v) break;
  27. }
  28. }
  29. }
  30. void Tarjan(int n) {
  31. cnt = top = dfn = 0;
  32. memset(dfn,0,sizeof(dfn));
  33. memset(low,0,sizeof(low));
  34. memset(sccno,0,sizeof(sccno));
  35. for(int i=1;i<=n;i++){
  36. if(!dfn[i])
  37. dfs(i);
  38. }
  39. }
  40. int main() {
  41. int n,m,u,v;
  42. while(cin>>n>>m && (n || m)) {
  43. for(int i=1;i<=n;i++) G[i].clear();
  44. for(int i=1;i<=m;i++) {
  45. cin>>u>>v;
  46. G[u].push_back(v);
  47. }
  48. Tarjan(n);
  49. cnt==1?cout<<"Yes"<<endl:cout<<"No"<<endl;
  50. }
  51. return 0;
  52. }

参考:Tarjan算法

发表评论

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

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

相关阅读

    相关 中的算法

    图论的概念:图论是数学的一个分支,它是以图为研究对象,图论中的图是由若干个给定的点及连接两点的线所构成的图形,这种图形通常用来描述某些实体之间的某种特定的关系,用点代表实体,用

    相关 Tarjan 算法

    Tarjan 算法 一.算法简介 Tarjan 算法一种由Robert Tarjan提出的求解有向图强连通分量的算法,它能做到线性时间的复杂度。   我们定义: 如果

    相关 Tarjan算法

     下面详细介绍一下Tarjan算法的基本思路:       1.任选一个点为根节点,从根节点开始。       2.遍历该点u所有子节点v,并标记这些子节点v已被访问

    相关 算法

    Problem1一笔画问题 题目描述     给出一个图,求其欧拉回路(若没有回路,则求其欧拉路径),若不存在则输出‘No solution’ 输入     输入的第一

    相关 Floyd算法

    Floyd算法     时间复杂度O (n^3)   空间复杂度O (n^2) 用处   可以求任意两个点之间的最短路径长度。   得出的邻接矩阵存储 i 到 j 的