• 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点之间都存在路径的,有些是部分节点连通,我们称这样的图为非强连通图,其中的部分强连通子图,就称为强连通分量。

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


    在上面这个图中,{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:

    1 2
    1 3
    2 4
    3 4
    3 5
    4 1
    4 6
    5 6

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

    有向图类Graph.java:

    package Tarjan;
    
    import java.util.ArrayList;
    
    /**
     * 有向图类
     * 
     * @author lyq
     * 
     */
    public class Graph {
    	// 图包含的点的标号
    	ArrayList<Integer> vertices;
    	// 图包含的有向边的分布,edges[i][j]中,i,j代表的是图的标号
    	int[][] edges;
    	// 图数据
    	ArrayList<String[]> graphDatas;
    
    	public Graph(ArrayList<String[]> graphDatas) {
    		this.graphDatas = graphDatas;
    		vertices = new ArrayList<>();
    	}
    
    	/**
    	 * 利用图数据构造有向图
    	 */
    	public void constructGraph() {
    		int v1 = 0;
    		int v2 = 0;
    		int verticNum = 0;
    
    		for (String[] array : graphDatas) {
    			v1 = Integer.parseInt(array[0]);
    			v2 = Integer.parseInt(array[1]);
    
    			if (!vertices.contains(v1)) {
    				vertices.add(v1);
    			}
    
    			if (!vertices.contains(v2)) {
    				vertices.add(v2);
    			}
    		}
    
    		verticNum = vertices.size();
    		// 多申请1个空间,是标号和下标一致
    		edges = new int[verticNum + 1][verticNum + 1];
    
    		// 做边的初始化操作,-1 代表的是此方向没有连通的边
    		for (int i = 1; i < verticNum + 1; i++) {
    			for (int j = 1; j < verticNum + 1; j++) {
    				edges[i][j] = -1;
    			}
    		}
    
    		for (String[] array : graphDatas) {
    			v1 = Integer.parseInt(array[0]);
    			v2 = Integer.parseInt(array[1]);
    
    			edges[v1][v2] = 1;
    		}
    	}
    }
    
    算法工具类TarjanTool.java:

    package Tarjan;
    
    import java.io.BufferedReader;
    import java.io.File;
    import java.io.FileReader;
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.Stack;
    
    /**
     * Tarjan算法-有向图强连通分量算法
     * 
     * @author lyq
     * 
     */
    public class TarjanTool {
    	// 当前节点的遍历号
    	public static int currentSeq = 1;
    
    	// 图构造数据文件地址
    	private String graphFile;
    	// 节点u搜索的次序编号
    	private int DFN[];
    	// u或u的子树能回溯到的最早的节点的次序编号
    	private int LOW[];
    	// 由图数据构造的有向图
    	private Graph graph;
    	// 图遍历节点栈
    	private Stack<Integer> verticStack;
    	// 强连通分量结果
    	private ArrayList<ArrayList<Integer>> resultGraph;
    	// 图的未遍历的点的标号列表
    	private ArrayList<Integer> remainVertices;
    	// 图未遍历的边的列表
    	private ArrayList<int[]> remainEdges;
    
    	public TarjanTool(String graphFile) {
    		this.graphFile = graphFile;
    		readDataFile();
    	}
    
    	/**
    	 * 从文件中读取数据
    	 * 
    	 */
    	private void readDataFile() {
    		File file = new File(graphFile);
    		ArrayList<String[]> dataArray = new ArrayList<String[]>();
    
    		try {
    			BufferedReader in = new BufferedReader(new FileReader(file));
    			String str;
    			String[] tempArray;
    			while ((str = in.readLine()) != null) {
    				tempArray = str.split(" ");
    				dataArray.add(tempArray);
    			}
    			in.close();
    		} catch (IOException e) {
    			e.getStackTrace();
    		}
    
    		// 根据数据构造有向图
    		graph = new Graph(dataArray);
    		graph.constructGraph();
    	}
    	
    	/**
    	 * 初始化2个标量数组
    	 */
    	private void initDfnAndLow(){
    		int verticNum = 0;
    		verticStack = new Stack<>();
    		remainVertices = (ArrayList<Integer>) graph.vertices.clone();
    		remainEdges = new ArrayList<>();
    		resultGraph = new ArrayList<>();
    
    		for (int i = 0; i < graph.edges.length; i++) {
    			remainEdges.add(graph.edges[i]);
    		}
    
    		verticNum = graph.vertices.size();
    		DFN = new int[verticNum + 1];
    		LOW = new int[verticNum + 1];
    
    		// 初始化数组操作
    		for (int i = 1; i <= verticNum; i++) {
    			DFN[i] = Integer.MAX_VALUE;
    			LOW[i] = -1;
    		}
    	}
    
    	/**
    	 * 搜索强连通分量
    	 */
    	public void searchStrongConnectedGraph() {
    		int label = 0;
    		int verticNum = graph.vertices.size();
    		initDfnAndLow();
    		
    		// 设置第一个的DFN[1]=1;
    		DFN[1] = 1;
    		// 移除首个节点
    		label = remainVertices.get(0);
    
    		verticStack.add(label);
    		remainVertices.remove((Integer) 1);
    		while (remainVertices.size() > 0) {
    			for (int i = 1; i <= verticNum; i++) {
    				if (graph.edges[label][i] == 1) {
    					// 把与此边相连的节点也加入栈中
    					verticStack.add(i);
    					remainVertices.remove((Integer) i);
    
    					dfsSearch(verticStack);
    				}
    			}
    
    			LOW[label] = searchEarliestDFN(label);
    			// 重新回溯到第一个点进行DFN和LOW值的判断
    			if (LOW[label] == DFN[label]) {
    				popStackGraph(label);
    			}
    		}
    
    		printSCG();
    	}
    
    	/**
    	 * 深度优先遍历的方式寻找强连通分量
    	 * 
    	 * @param stack
    	 *            存放的节点的当前栈
    	 * @param seqNum
    	 *            当前遍历的次序号
    	 */
    	private void dfsSearch(Stack<Integer> stack) {
    		int currentLabel = stack.peek();
    		// 设置搜索次序号,在原先的基础上增加1
    		currentSeq++;
    		DFN[currentLabel] = currentSeq;
    		LOW[currentLabel] = searchEarliestDFN(currentLabel);
    
    		int[] edgeVertic;
    		edgeVertic = remainEdges.get(currentLabel);
    		for (int i = 1; i < edgeVertic.length; i++) {
    			if (edgeVertic[i] == 1) {
    				// 如果剩余可选节点中包含此节点吗,则此节点添加
    				if (remainVertices.contains(i)) {
    					stack.add(i);
    				} else {
    					// 不包含,则跳过
    					continue;
    				}
    
    				// 将与此边相连的点加入栈中
    				remainVertices.remove((Integer) i);
    				remainEdges.set(currentLabel, null);
    
    				// 继续深度优先遍历
    				dfsSearch(stack);
    			}
    		}
    
    		if (LOW[currentLabel] == DFN[currentLabel]) {
    			popStackGraph(currentLabel);
    		}
    
    	}
    
    	/**
    	 * 从栈中弹出局部结果
    	 * 
    	 * @param label
    	 *            弹出的临界标号
    	 */
    	private void popStackGraph(int label) {
    		// 如果2个值相等,则将此节点以及此节点后的点移出栈中
    		int value = 0;
    
    		ArrayList<Integer> scg = new ArrayList<>();
    		while (label != verticStack.peek()) {
    			value = verticStack.pop();
    			scg.add(0, value);
    		}
    		scg.add(0, verticStack.pop());
    
    		resultGraph.add(scg);
    	}
    
    	/**
    	 * 当前的节点可能搜索到的最早的次序号
    	 * 
    	 * @param label
    	 *            当前的节点标号
    	 * @return
    	 */
    	private int searchEarliestDFN(int label) {
    		// 判断此节点是否有子边
    		boolean hasSubEdge = false;
    		int minDFN = DFN[label];
    
    		// 如果搜索到的次序号已经是最小的次序号,则返回
    		if (DFN[label] == 1) {
    			return DFN[label];
    		}
    
    		int tempDFN = 0;
    		for (int i = 1; i <= graph.vertices.size(); i++) {
    			if (graph.edges[label][i] == 1) {
    				hasSubEdge = true;
    
    				// 如果在堆栈中和剩余节点中都未包含此节点说明已经被退栈了,不允许再次遍历
    				if (!remainVertices.contains(i) && !verticStack.contains(i)) {
    					continue;
    				}
    				tempDFN = searchEarliestDFN(i);
    
    				if (tempDFN < minDFN) {
    					minDFN = tempDFN;
    				}
    			}
    		}
    
    		// 如果没有子边,则搜索到的次序号就是它自身
    		if (!hasSubEdge && DFN[label] != -1) {
    			minDFN = DFN[label];
    		}
    
    		return minDFN;
    	}
    	
    	/**
    	 * 标准搜索强连通分量算法
    	 */
    	public void standardSearchSCG(){
    		initDfnAndLow();
    		
    		verticStack.add(1);
    		remainVertices.remove((Integer)1);
    		//从标号为1的第一个节点开始搜索
    		dfsSearchSCG(1);
    		
    		//输出结果中的强连通分量
    		printSCG();
    	}
    
    	/**
    	 * 深度优先搜索强连通分量
    	 * 
    	 * @param u
    	 *            当前搜索的节点标号
    	 */
    	private void dfsSearchSCG(int u) {
    		DFN[u] = currentSeq;
    		LOW[u] = currentSeq;
    		currentSeq++;
    
    		for (int i = 1; i <graph.edges[u].length; i++) {
    			// 判断u,i两节点是否相连
    			if (graph.edges[u][i] == 1) {
    				// 相连的情况下,当i未被访问过的时候,加入栈中
    				if (remainVertices.contains(i)) {
    					verticStack.add(i);
    					remainVertices.remove((Integer) i);
    					// 递归搜索
    					dfsSearchSCG(i);
    					LOW[u] = (LOW[u] < LOW[i] ? LOW[u] : LOW[i]);
    				} else if(verticStack.contains(i)){
    					// 如果已经访问过,并且还未出栈过的
    					LOW[u] = (LOW[u] < DFN[i] ? LOW[u] : DFN[i]);
    					//LOW[u] = (LOW[u] < LOW[i] ? LOW[u] : LOW[i]); 如果都用LOW做判断,也可以通过测试
    				}
    			}
    		}
    
    		// 最后判断DFN和LOW是否相等
    		if (DFN[u] == LOW[u]) {
    			popStackGraph(u);
    		}
    	}
    
    	/**
    	 * 输出有向图中的强连通分量
    	 */
    	private void printSCG() {
    		int i = 1;
    		String resultStr = "";
    		System.out.println("所有强连通分量子图:");
    		for (ArrayList<Integer> graph : resultGraph) {
    			resultStr = "";
    			resultStr += "强连通分量" + i + ":{";
    			for (Integer v : graph) {
    				resultStr += (v + ", ");
    			}
    			resultStr = (String) resultStr.subSequence(0,
    					resultStr.length() - 2);
    			resultStr += "}";
    
    			System.out.println(resultStr);
    			i++;
    		}
    	}
    }
    
    测试类Client.java:

    package Tarjan;
    
    /**
     * Tarjan算法--有向图强连通分量算法
     * @author lyq
     *
     */
    public class Client {
    	public static void main(String[] args){
    		//图构造数据文件地址
    		String graphFilePath = "C:\Users\lyq\Desktop\icon\graphData.txt";
    		
    		TarjanTool tool = new TarjanTool(graphFilePath);
    		//下面这个方法为改造的一点方法,还有点问题
    		//tool.searchStrongConnectedGraph();
    		tool.standardSearchSCG();
    	}
    }
    

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


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

    算法的输出结果:

    所有强连通分量子图:
    强连通分量1:{6}
    强连通分量2:{5}
    强连通分量3:{1, 2, 4, 3}

    算法的遗漏点

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

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

    算法的突破点

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

  • 相关阅读:
    计算几何 判断点在直线的左右哪一侧
    图论 迪杰斯特拉dijkstra求最短路径
    图论 用prim法求最小生成树
    图论 邻接表广搜
    图论 用广搜搜邻接矩阵
    图论 邻接表建图+dfs
    图论 邻接矩阵建图+dfs遍历
    HDU 2141 二分查找
    二叉树知道前序和中序求后序,知道中序后序求中序
    二叉树的查找
  • 原文地址:https://www.cnblogs.com/bianqi/p/12183979.html
Copyright © 2020-2023  润新知