Kosaraju算法查找有向图的强连通分支

部分转载自:http://www.tuicool.com/articles/uQBz2y

1.相关概念

给定一个有向图 G = (V, E),对于任意一对顶点 u 和 v,有 u –> v 和 v –> u,亦即,顶点 u 和 v 是互相可达的,则说明该图 G 是强连通的(Strongly Connected)。如下图中,任意两个顶点都是互相可达的。

《Kosaraju算法查找有向图的强连通分支》

对于无向图,判断图是否是强连通的,可以直接使用深度优先搜索(DFS)或广度优先搜索(BFS),从任意一个顶点出发,如果遍历的结果包含所有的顶点,则说明图是强连通的。

而对于有向图,则不能使用 DFS 或 BFS 进行直接遍历来判断。如下图中,如果从顶点 0 开始遍历则可判断是强连通的,而如果从其它顶点开始遍历则无法抵达所有节点。

《Kosaraju算法查找有向图的强连通分支》

那么,该如何判断有向图的强连通性呢?

实际上,解决该问题的较好的方式就是使用 强连通分支算法(SCC:Strongly Connected Components) ,可以在  O(V+E) 时间内找到所有的 SCC。如果 SCC 的数量是 1,则说明整个图是强连通的。

有向图 G = (V, E) 的一个强连通分支是一个最大的顶点集合 C,C 是 V 的子集,对于 C 中的每一对顶点 u 和 v,有 u –> v 和 v –> u,亦即,顶点 u 和 v 是互相可达的。

实现 SCC 的一种算法就是 Kosaraju 算法 。Kosaraju 算法基于深度优先搜索(DFS),并对图进行两次 DFS 遍历,算法步骤如下:

  1. 初始化设置所有的顶点为未访问的;
  2. 从任意顶点 v 开始进行 DFS 遍历,如果遍历结果没有访问到所有顶点,则说明图不是强连通的;
  3. 置换整个图(Reverse Graph);
  4. 设置置换后的图中的所有顶点为未访问过的;
  5. 从与步骤 2 中相同的顶点 v 开始做 DFS 遍历,如果遍历没有访问到所有顶点,则说明图不是强连通的,否则说明图是强连通的。

Kosaraju 算法的思想就是,如果从顶点 v 可以抵达所有顶点,并且所有顶点都可以抵达 v,则说明图是强连通的。

2. java源代码

/**找到有向图的强连通分支,正向遍历,按照后根序压栈,根据反向图查看结点是否可达*/
import java.util.*;
public class Kosaraju {
	
   static class Graph
   {
	   int n;
	   List<Integer>[] adj;
	   Graph(int n)
	   {
		   this.n=n;
		   this.adj=new List[n];
		   for(int i=0;i<n;i++)
		   {
			   this.adj[i]=new ArrayList<Integer>();
		   }
	   }
	   
	   public void addEdge(int v,int w)
	   {
		   this.adj[v].add(w);
	   }
	   
	   /**正向遍历,以后根序压栈,保证根先出栈*/
	   public void fillorder(int v,boolean[] visited,Stack<Integer> s)
	   {
		   visited[v]=true;
		   for(Integer i:this.adj[v])
		   {
			   if(!visited[i])
			   {
				   fillorder(i,visited,s);
			   }
		   }
		   s.push(v);
	   }
	   /**得到反向图*/
	   public Graph getTranspose()
	   {
		  Graph gv=new Graph(this.n);
		  for(int i=0;i<n;i++)
		  {
			  for(Integer j:this.adj[i])
			  {
				  gv.adj[j].add(i);
			  }
		  }
		  return gv;
	   }
	   
	   /**DFS打印连通分支*/
	   public void DFSUtil(int v,boolean[] visited)
	   {
		   visited[v]=true;
		   System.out.print(v+" ");
		   for(Integer i:adj[v])
		   {
			   if(!visited[i])
			   {
				   DFSUtil(i,visited);
			   }
		   }
	   }
	   
	   /**按照Kosaraju算法的步骤执行*/
	   public void printSCCs()
	   {
		   Stack<Integer> s=new Stack<Integer>();
		   boolean[] visited=new boolean[this.n];
		   for(int i=0;i<n;i++)
		   {
			   visited[i]=false;
		   }
		   /**后根序压栈*/
		   for(int i=0;i<n;i++)
		   {
			   if(!visited[i])
			   {
				   fillorder(i,visited,s);
			   }
		   }
		   /**得到反向图*/
		   Graph gr=this.getTranspose();
		   for(int i=0;i<n;i++)
		   {
			   visited[i]=false;
		   }
		   /**依据反向图算可达性*/
		   while(!s.empty())
		   {
			   int v=s.pop();
			   if(visited[v]==false)
			   {
				   gr.DFSUtil(v, visited);
				   System.out.println();
			   }
			   
		   }
	   }
   }
   
   public static void main(String args[])
   {
	   Graph g=new Graph(5);
	   g.addEdge(1, 0);
	   g.addEdge(0, 2);
	   g.addEdge(2, 1);
	   g.addEdge(3, 0);
	   g.addEdge(3, 4);
	   g.printSCCs();
   }
   
  
}

3. 正确性证明

1. 第一次DFS有向图G时,最后记录下的节点必为最后一棵生成树的根节点。 
证明:假设最后记录下节点不是树根,则必存在一节点为树根,且树根节点必为此节点祖先;而由后根序访问可知祖先节点比此节点更晚访问,矛盾;原命题成立

2. 第一次DFS的生成森林中,取两节点A、B,满足:B比A更晚记录下,且B不是A的祖先(即在第一次DFS中,A、B处于不同的生成树中);则在第二次DFS的生成森林中,B不是A的祖先,且A也不是B的祖先(即在第二次DFS中,A、B处于不同的生成树中)。 
证明:假设在第二次DFS的生成森林中,B是A的祖先,则反图GT中存在B到A路径,即第一次DFS生成森林中,A是B的祖先,则A必比B更晚记录下,矛盾;假设在第二次DFS的生成森林中,A是B的祖先,则反图GT中存在A到B路径,即第一次DFS生成森林中,B是A的祖先,矛盾;原命题成立

3. 按上述步骤求出的必为强连通分量 
证明:首先,证明2保证了第二次DFS中的每一棵树都是第一次DFS中的某棵树或某棵树的子树。其次,对于第二次DFS中的每棵树,第一次DFS保证了从根到其子孙的连通性,第二次DFS保证了根到子孙的反向连通性(即子孙到根的连通性);由此,此树中的每个节点都通过其根相互连通。

4. 算法复杂度

DFS遍历的时间复杂度为O(V+E) ,图的转置也为O(V+E) 。因此总的时间复杂度为O(V+E)。

    原文作者:查找算法
    原文地址: https://blog.csdn.net/tingting256/article/details/50496810
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞