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

参考链接:https://www.byvoid.com/blog/scc-tarjan/
我的算法库:https://github.com/linyiqun/lyq-algorithms-lib

算法介绍

正如标题所介绍的那样,Tarjan算法的目标就是找出图中的连通图的,其实前提条件是这样的,在一个有向图中,有时必然会出现节点成环的情况,而在图内部的这些形成环的节点所构成的图就是我们所要找的,这个在实际生活中非常的有用,可以帮助我们解决最短距离之类的问题。

算法原理

概念

在介绍算法原理的之前,必须先明白一些概念:

1、强连通。在有向图G中,如果2个点之间存在至少一条路径,我们称这2个点为强连通。

2、强连通图。在图G中,如果其中任意的2个点都是强连通,则称图G为强连通图。

3、强连通分量。并不是所有的图中的任意2点之间都存在路径的,有些是部分节点连通,我们称这样的图为非强连通图,其中的部分强连通子图,就称为强连通分量。

强连通分量就是本次算法要找的东西。下面给出一个图示:

Tarjan算法--有向图强连通分量算法_第1张图片

在上面这个图中,{1, 2, 3, 4}是强连通分量,因为5,6是达不到的对于1, 2, 3, 4,来说,这里也将5,6单独作为强连通分量,可以理解为自己到自己是可达的(这样解释感觉比较勉强,但是定义也是允许这样的情况的)。

算法的过程

算法为每个节点定义了2个变量DFN[i]和LOW[i],DFN[i]代表的意思是i节点的搜索次序号,LOW[i]代表的是i节点或i的子节点能够追溯到的最早的节点的次序号,如果这么说没有理解的话,没有关系,可以看下面的伪代码:

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)                   // 如果节点v还在栈内
            Low[u] = min(Low[u], DFN[v])
    if (DFN[u] == Low[u])                      // 如果节点u是强连通分量的根
        repeat
            v = S.pop                  // 将v退栈,为该强连通分量中一个顶点
            print v
        until (u== v)
}

算法的实现

算法的实现采用的例子还是上面这个例子,输入数据graphData.txt:

[java]  view plain copy print ?
  1. 1 2  
  2. 1 3  
  3. 2 4  
  4. 3 4  
  5. 3 5  
  6. 4 1  
  7. 4 6  
  8. 5 6  

输入格式为标号1 标号2,代表的意思是存在标号1指向标号2节点的边。

有向图类Graph.java:

[java]  view plain copy print ?
  1. package Tarjan;  
  2.   
  3. import java.util.ArrayList;  
  4.   
  5. /** 
  6.  * 有向图类 
  7.  *  
  8.  * @author lyq 
  9.  *  
  10.  */  
  11. public class Graph {  
  12.     // 图包含的点的标号  
  13.     ArrayList<Integer> vertices;  
  14.     // 图包含的有向边的分布,edges[i][j]中,i,j代表的是图的标号  
  15.     int[][] edges;  
  16.     // 图数据  
  17.     ArrayList<String[]> graphDatas;  
  18.   
  19.     public Graph(ArrayList<String[]> graphDatas) {  
  20.         this.graphDatas = graphDatas;  
  21.         vertices = new ArrayList<>();  
  22.     }  
  23.   
  24.     /** 
  25.      * 利用图数据构造有向图 
  26.      */  
  27.     public void constructGraph() {  
  28.         int v1 = 0;  
  29.         int v2 = 0;  
  30.         int verticNum = 0;  
  31.   
  32.         for (String[] array : graphDatas) {  
  33.             v1 = Integer.parseInt(array[0]);  
  34.             v2 = Integer.parseInt(array[1]);  
  35.   
  36.             if (!vertices.contains(v1)) {  
  37.                 vertices.add(v1);  
  38.             }  
  39.   
  40.             if (!vertices.contains(v2)) {  
  41.                 vertices.add(v2);  
  42.             }  
  43.         }  
  44.   
  45.         verticNum = vertices.size();  
  46.         // 多申请1个空间,是标号和下标一致  
  47.         edges = new int[verticNum + 1][verticNum + 1];  
  48.   
  49.         // 做边的初始化操作,-1 代表的是此方向没有连通的边  
  50.         for (int i = 1; i < verticNum + 1; i++) {  
  51.             for (int j = 1; j < verticNum + 1; j++) {  
  52.                 edges[i][j] = -1;  
  53.             }  
  54.         }  
  55.   
  56.         for (String[] array : graphDatas) {  
  57.             v1 = Integer.parseInt(array[0]);  
  58.             v2 = Integer.parseInt(array[1]);  
  59.   
  60.             edges[v1][v2] = 1;  
  61.         }  
  62.     }  
  63. }  
算法工具类TarjanTool.java:

[java]  view plain copy print ?
  1. package Tarjan;  
  2.   
  3. import java.io.BufferedReader;  
  4. import java.io.File;  
  5. import java.io.FileReader;  
  6. import java.io.IOException;  
  7. import java.util.ArrayList;  
  8. import java.util.Stack;  
  9.   
  10. /** 
  11.  * Tarjan算法-有向图强连通分量算法 
  12.  *  
  13.  * @author lyq 
  14.  *  
  15.  */  
  16. public class TarjanTool {  
  17.     // 当前节点的遍历号  
  18.     public static int currentSeq = 1;  
  19.   
  20.     // 图构造数据文件地址  
  21.     private String graphFile;  
  22.     // 节点u搜索的次序编号  
  23.     private int DFN[];  
  24.     // u或u的子树能回溯到的最早的节点的次序编号  
  25.     private int LOW[];  
  26.     // 由图数据构造的有向图  
  27.     private Graph graph;  
  28.     // 图遍历节点栈  
  29.     private Stack<Integer> verticStack;  
  30.     // 强连通分量结果  
  31.     private ArrayList<ArrayList<Integer>> resultGraph;  
  32.     // 图的未遍历的点的标号列表  
  33.     private ArrayList<Integer> remainVertices;  
  34.     // 图未遍历的边的列表  
  35.     private ArrayList<int[]> remainEdges;  
  36.   
  37.     public TarjanTool(String graphFile) {  
  38.         this.graphFile = graphFile;  
  39.         readDataFile();  
  40.     }  
  41.   
  42.     /** 
  43.      * 从文件中读取数据 
  44.      *  
  45.      */  
  46.     private void readDataFile() {  
  47.         File file = new File(graphFile);  
  48.         ArrayList<String[]> dataArray = new ArrayList<String[]>();  
  49.   
  50.         try {  
  51.             BufferedReader in = new BufferedReader(new FileReader(file));  
  52.             String str;  
  53.             String[] tempArray;  
  54.             while ((str = in.readLine()) != null) {  
  55.                 tempArray = str.split(" ");  
  56.                 dataArray.add(tempArray);  
  57.             }  
  58.             in.close();  
  59.         } catch (IOException e) {  
  60.             e.getStackTrace();  
  61.         }  
  62.   
  63.         // 根据数据构造有向图  
  64.         graph = new Graph(dataArray);  
  65.         graph.constructGraph();  
  66.     }  
  67.       
  68.     /** 
  69.      * 初始化2个标量数组 
  70.      */  
  71.     private void initDfnAndLow(){  
  72.         int verticNum = 0;  
  73.         verticStack = new Stack<>();  
  74.         remainVertices = (ArrayList<Integer>) graph.vertices.clone();  
  75.         remainEdges = new ArrayList<>();  
  76.         resultGraph = new ArrayList<>();  
  77.   
  78.         for (int i = 0; i < graph.edges.length; i++) {  
  79.             remainEdges.add(graph.edges[i]);  
  80.         }  
  81.   
  82.         verticNum = graph.vertices.size();  
  83.         DFN = new int[verticNum + 1];  
  84.         LOW = new int[verticNum + 1];  
  85.   
  86.         // 初始化数组操作  
  87.         for (int i = 1; i <= verticNum; i++) {  
  88.             DFN[i] = Integer.MAX_VALUE;  
  89.             LOW[i] = -1;  
  90.         }  
  91.     }  
  92.   
  93.     /** 
  94.      * 搜索强连通分量 
  95.      */  
  96.     public void searchStrongConnectedGraph() {  
  97.         int label = 0;  
  98.         int verticNum = graph.vertices.size();  
  99.         initDfnAndLow();  
  100.           
  101.         // 设置第一个的DFN[1]=1;  
  102.         DFN[1] = 1;  
  103.         // 移除首个节点  
  104.         label = remainVertices.get(0);  
  105.   
  106.         verticStack.add(label);  
  107.         remainVertices.remove((Integer) 1);  
  108.         while (remainVertices.size() > 0) {  
  109.             for (int i = 1; i <= verticNum; i++) {  
  110.                 if (graph.edges[label][i] == 1) {  
  111.                     // 把与此边相连的节点也加入栈中  
  112.                     verticStack.add(i);  
  113.                     remainVertices.remove((Integer) i);  
  114.   
  115.                     dfsSearch(verticStack);  
  116.                 }  
  117.             }  
  118.   
  119.             LOW[label] = searchEarliestDFN(label);  
  120.             // 重新回溯到第一个点进行DFN和LOW值的判断  
  121.             if (LOW[label] == DFN[label]) {  
  122.                 popStackGraph(label);  
  123.             }  
  124.         }  
  125.   
  126.         printSCG();  
  127.     }  
  128.   
  129.     /** 
  130.      * 深度优先遍历的方式寻找强连通分量 
  131.      *  
  132.      * @param stack 
  133.      *            存放的节点的当前栈 
  134.      * @param seqNum 
  135.      *            当前遍历的次序号 
  136.      */  
  137.     private void dfsSearch(Stack<Integer> stack) {  
  138.         int currentLabel = stack.peek();  
  139.         // 设置搜索次序号,在原先的基础上增加1  
  140.         currentSeq++;  
  141.         DFN[currentLabel] = currentSeq;  
  142.         LOW[currentLabel] = searchEarliestDFN(currentLabel);  
  143.   
  144.         int[] edgeVertic;  
  145.         edgeVertic = remainEdges.get(currentLabel);  
  146.         for (int i = 1; i < edgeVertic.length; i++) {  
  147.             if (edgeVertic[i] == 1) {  
  148.                 // 如果剩余可选节点中包含此节点吗,则此节点添加  
  149.                 if (remainVertices.contains(i)) {  
  150.                     stack.add(i);  
  151.                 } else {  
  152.                     // 不包含,则跳过  
  153.                     continue;  
  154.                 }  
  155.   
  156.                 // 将与此边相连的点加入栈中  
  157.                 remainVertices.remove((Integer) i);  
  158.                 remainEdges.set(currentLabel, null);  
  159.   
  160.                 // 继续深度优先遍历  
  161.                 dfsSearch(stack);  
  162.             }  
  163.         }  
  164.   
  165.         if (LOW[currentLabel] == DFN[currentLabel]) {  
  166.             popStackGraph(currentLabel);  
  167.         }  
  168.   
  169.     }  
  170.   
  171.     /** 
  172.      * 从栈中弹出局部结果 
  173.      *  
  174.      * @param label 
  175.      *            弹出的临界标号 
  176.      */  
  177.     private void popStackGraph(int label) {  
  178.         // 如果2个值相等,则将此节点以及此节点后的点移出栈中  
  179.         int value = 0;  
  180.   
  181.         ArrayList<Integer> scg = new ArrayList<>();  
  182.         while (label != verticStack.peek()) {  
  183.             value = verticStack.pop();  
  184.             scg.add(0, value);  
  185.         }  
  186.         scg.add(0, verticStack.pop());  
  187.   
  188.         resultGraph.add(scg);  
  189.     }  
  190.   
  191.     /** 
  192.      * 当前的节点可能搜索到的最早的次序号 
  193.      *  
  194.      * @param label 
  195.      *            当前的节点标号 
  196.      * @return 
  197.      */  
  198.     private int searchEarliestDFN(int label) {  
  199.         // 判断此节点是否有子边  
  200.         boolean hasSubEdge = false;  
  201.         int minDFN = DFN[label];  
  202.   
  203.         // 如果搜索到的次序号已经是最小的次序号,则返回  
  204.         if (DFN[label] == 1) {  
  205.             return DFN[label];  
  206.         }  
  207.   
  208.         int tempDFN = 0;  
  209.         for (int i = 1; i <= graph.vertices.size(); i++) {  
  210.             if (graph.edges[label][i] == 1) {  
  211.                 hasSubEdge = true;  
  212.   
  213.                 // 如果在堆栈中和剩余节点中都未包含此节点说明已经被退栈了,不允许再次遍历  
  214.                 if (!remainVertices.contains(i) && !verticStack.contains(i)) {  
  215.                     continue;  
  216.                 }  
  217.                 tempDFN = searchEarliestDFN(i);  
  218.   
  219.                 if (tempDFN < minDFN) {  
  220.                     minDFN = tempDFN;  
  221.                 }  
  222.             }  
  223.         }  
  224.   
  225.         // 如果没有子边,则搜索到的次序号就是它自身  
  226.         if (!hasSubEdge && DFN[label] != -1) {  
  227.             minDFN = DFN[label];  
  228.         }  
  229.   
  230.         return minDFN;  
  231.     }  
  232.       
  233.     /** 
  234.      * 标准搜索强连通分量算法 
  235.      */  
  236.     public void standardSearchSCG(){  
  237.         initDfnAndLow();  
  238.           
  239.         verticStack.add(1);  
  240.         remainVertices.remove((Integer)1);  
  241.         //从标号为1的第一个节点开始搜索  
  242.         dfsSearchSCG(1);  
  243.           
  244.         //输出结果中的强连通分量  
  245.         printSCG();  
  246.     }  
  247.   
  248.     /** 
  249.      * 深度优先搜索强连通分量 
  250.      *  
  251.      * @param u 
  252.      *            当前搜索的节点标号 
  253.      */  
  254.     private void dfsSearchSCG(int u) {  
  255.         DFN[u] = currentSeq;  
  256.         LOW[u] = currentSeq;  
  257.         currentSeq++;  
  258.   
  259.         for (int i = 1; i <graph.edges[u].length; i++) {  
  260.             // 判断u,i两节点是否相连  
  261.             if (graph.edges[u][i] == 1) {  
  262.                 // 相连的情况下,当i未被访问过的时候,加入栈中  
  263.                 if (remainVertices.contains(i)) {  
  264.                     verticStack.add(i);  
  265.                     remainVertices.remove((Integer) i);  
  266.                     // 递归搜索  
  267.                     dfsSearchSCG(i);  
  268.                     LOW[u] = (LOW[u] < LOW[i] ? LOW[u] : LOW[i]);  
  269.                 } else if(verticStack.contains(i)){  
  270.                     // 如果已经访问过,并且还未出栈过的  
  271.                     LOW[u] = (LOW[u] < DFN[i] ? LOW[u] : DFN[i]);  
  272.                     //LOW[u] = (LOW[u] < LOW[i] ? LOW[u] : LOW[i]); 如果都用LOW做判断,也可以通过测试  
  273.                 }  
  274.             }  
  275.         }  
  276.   
  277.         // 最后判断DFN和LOW是否相等  
  278.         if (DFN[u] == LOW[u]) {  
  279.             popStackGraph(u);  
  280.         }  
  281.     }  
  282.   
  283.     /** 
  284.      * 输出有向图中的强连通分量 
  285.      */  
  286.     private void printSCG() {  
  287.         int i = 1;  
  288.         String resultStr = "";  
  289.         System.out.println("所有强连通分量子图:");  
  290.         for (ArrayList<Integer> graph : resultGraph) {  
  291.             resultStr = "";  
  292.             resultStr += "强连通分量" + i + ":{";  
  293.             for (Integer v : graph) {  
  294.                 resultStr += (v + ", ");  
  295.             }  
  296.             resultStr = (String) resultStr.subSequence(0,  
  297.                     resultStr.length() - 2);  
  298.             resultStr += "}";  
  299.   
  300.             System.out.println(resultStr);  
  301.             i++;  
  302.         }  
  303.     }  
  304. }  
测试类Client.java:

[java]  view plain copy print ?
  1. package Tarjan;  
  2.   
  3. /** 
  4.  * Tarjan算法--有向图强连通分量算法 
  5.  * @author lyq 
  6.  * 
  7.  */  
  8. public class Client {  
  9.     public static void main(String[] args){  
  10.         //图构造数据文件地址  
  11.         String graphFilePath = "C:\\Users\\lyq\\Desktop\\icon\\graphData.txt";  
  12.           
  13.         TarjanTool tool = new TarjanTool(graphFilePath);  
  14.         //下面这个方法为改造的一点方法,还有点问题  
  15.         //tool.searchStrongConnectedGraph();  
  16.         tool.standardSearchSCG();  
  17.     }  
  18. }  

算法的执行步骤如图所示(手机拍摄的截图效果不佳,请不要见怪):

Tarjan算法--有向图强连通分量算法_第2张图片

主要展示了随着遍历的顺序DFN和LOW数组的赋值情况,以及栈的内容变化情况

算法的输出结果:

[java]  view plain copy print ?
  1. 所有强连通分量子图:  
  2. 强连通分量1:{6}  
  3. 强连通分量2:{5}  
  4. 强连通分量3:{1243}  

算法的遗漏点

在这个算法中,我写了2个算法,searchStrongConnectGraph是我自己在没有看伪代码写的,后来发现,意思有点曲解了,遇到循环图的时候也会有问题,后来马上看了伪代码,马上代码精简了很多,的确是非常强大的算法,第二点是我觉得在下面这个步骤中,判断是否可以合并在一起,因为我发现结果是一致的,都可以用LOW数组的值来判断。

[java]  view plain copy print ?
  1. // 相连的情况下,当i未被访问过的时候,加入栈中  
  2.                 if (remainVertices.contains(i)) {  
  3.                     verticStack.add(i);  
  4.                     remainVertices.remove((Integer) i);  
  5.                     // 递归搜索  
  6.                     dfsSearchSCG(i);  
  7.                     LOW[u] = (LOW[u] < LOW[i] ? LOW[u] : LOW[i]);  
  8.                 } else if(verticStack.contains(i)){  
  9.                     // 如果已经访问过,并且还未出栈过的  
  10.                     LOW[u] = (LOW[u] < DFN[i] ? LOW[u] : DFN[i]);  
  11.                     //LOW[u] = (LOW[u] < LOW[i] ? LOW[u] : LOW[i]); 如果都用LOW做判断,也可以通过测试  
  12.                 }  
可能是为了让图中的边只允许被遍历一次的原因吧。

算法的突破点

很明显算法的突破口在于比较LOW和DFN的值,因为DFN的值在遍历顺序的就已经确定,所以问题的关键在于LOW值的确定,因为题目的要求是找到最早的那个搜索号,在这里会采用深度优先的方式一层层的寻找,如果找到的小的,就进行替换,如果最后找到的还是他自己的时候,说明这中间其实是一个环。然后把栈中当前节点的上方把节点全部移出。

你可能感兴趣的:(Tarjan算法--有向图强连通分量算法)