Kosaraju算法解析: 求解图的强连通分量

1. 定义

连通分量:在无向图中,即为连通子图。

上图中,总共有四个连通分量。顶点A、B、C、D构成了一个连通分量,顶点E构成了一个连通分量,顶点F,G和H,I分别构成了两个连通分量。

强连通分量:有向图中,尽可能多的若干顶点组成的子图中,这些顶点都是相互可到达的,则这些顶点成为一个强连通分量。

上图中有三个强连通分量,分别是a、b、e以及f、g和c、d、h。

2. 连通分量的求解方法

对于一个无向图的连通分量,从连通分量的任意一个顶点开始,进行一次DFS,一定能遍历这个连通分量的所有顶点。所以,整个图的连通分量数应该等价于遍历整个图进行了几次(最外层的)DFS。一次DFS中遍历的所有顶点属于同一个连通分量。

下面我们将介绍有向图的强连通分量的求解方法。

3. Kosaraju算法的基本原理

我们用一个最简单的例子讲解Kosaraju算法

显然上图中有两个强连通分量,即强连通分量A和强连通分量B,分别由顶点A0-A1-A2和顶点B3-B4-B5构成。每个连通分量中有若干个可以相互访问的顶点(这里都是3个),强连通分量与强连通分量之间不会形成环,否则应该将这些连通分量看成一个整体,即看成同一个强连通分量。

我们现在试想能否按照无向图中求连通分量的思路求解有向图的强连通分量。我们假设,DFS从强连通分量B的任意一个顶点开始,那么恰好遍历整个图需要2次DFS,和连通分量的数量相等,而且每次DFS遍历的顶点恰好属于同一个连通分量。但是,我们若从连通分量A中任意一个顶点开始DFS,就不能得到正确的结果,因为此时我们只需要一次DFS就访问了所有的顶点。所以,我们不应该按照顶点编号的自然顺序(0,1,2,……)或者任意其它顺序进行DFS,而是应该按照被指向的强连通分量的顶点排在前面的顺序进行DFS。上图中由强连通分量A指向了强连通分量B。所以,我们按照

B3, B4, B5, A0, A1, A2

的顺序进行DFS,这样就可以达到我们的目的。但事实上这样的顺序太过严格,我们只需要保证被指向的强连通分量的至少一个顶点排在指向这个连通分量的所有顶点前面即可,比如

B3, A0, A1, A2, B4, B5

B3排在了强连通分量A所有顶点的前面。

现在我们的关键问题就是如何得到这样一个满足要求的顶点顺序,Kosaraju给出了这解决办法:对原图取反,然后从反向图的任意节点开始进行DFS的逆后序遍历,逆后序得到的顺序一定满足我们的要求。

DFS的逆后序遍历是指:如果当前顶点未访问,先遍历完与当前顶点相连的且未被访问的所有其它顶点,然后将当前顶点加入栈中,最后栈中从栈顶到栈底的顺序就是我们需要的顶点顺序。

上图表示原图的反向。

我们现在进行第一种假设:假设DFS从位于强连通分量A中的任意一个节点开始。那么第一次DFS完成后,栈中全部都是强连通分量A的顶点,第二次DFS完成后,栈顶一定是强连通分量B的顶点。保证了从栈顶到栈底的排序强连通分量B的顶点全部都在强连通分量A顶点之前。

我们现在进行第二种假设:假设DFS从位于强连通分量B中的任意一个顶点开始。显然我们只需要进行一次DFS就可以遍历整个图,由于是逆后续遍历,那么起始顶点一定最后完成,所以栈顶的顶点一定是强连通分量B中的顶点,这显然是我们希望得到的顶点排序的结果。

上面使用了最简单的例子说明Kosaraju算法的原理,对于有多个强连通分量,连接复杂的情况,仍然适用。大家可以自行举例验证。

综上可得,不论从哪个顶点开始,图中有多少个强连通分量,逆后续遍历的栈中顶点的顺序一定会保证:被指向的强连通分量的至少一个顶点排在指向这个连通分量的所有顶点前面。所以,我们求解强连通分量的步骤可以分为两步:

(1)对原图取反,从任意一个顶点开始对反向图进行逆后续DFS遍历

(2)按照逆后续遍历中栈中的顶点出栈顺序,对原图进行DFS遍历,一次DFS遍历中访问的所有顶点都属于同一强连通分量。

4. 求解连通分量和强连通分量的代码实现

测试数据


10

15

0 1

0 4

1 0

1 8

2 1

2 4

2 7

3 4

4 3

5 0

5 6

7 9

7 4

8 5

9 2


运行结果


图的表示

0 : 1 4

1 : 0 8

2 : 1 4 7

3 : 4

4 : 3

5 : 0 6

6 :

7 : 9 4

8 : 5

9 : 2

连通分量数: 4

和顶点 0 共属于同一个连通分量的顶点

0 1 5 8

和顶点 3 共属于同一个连通分量的顶点

3 4

和顶点 9 共属于同一个连通分量的顶点

2 7 9

和顶点 6 共属于同一个连通分量的顶点

6

ConnectedComponents 包含了无向图求连通分量以及Kosaraju算法的实现

package datastruct;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.LinkedList;
import java.util.List;

import datastruct.Graph.Edge;

public class ConnectedComponents {
	private boolean[] marked;
	/*用于标记每个顶点属于哪一个(强)连通分量
                 同一(强)连通分量顶点的(强)连通分量编号值相同*/
	private int[] id;
	private int count;//(强)连通分量的编号,也表示(强)连通分量的个数
	private Graph g;

	public ConnectedComponents(Graph g){
		this.g = g;
		marked = new boolean[g.V()];
		id = new int[g.V()];

		if(g.isDirect()){//有向图求强连通分量的方法
			//反向图DFS的逆后序,从0号顶点开始,可以从任意顶点开始
			LinkedList<Integer> stack = g.reverse().reversePostOrder(0);
			marked = new boolean[g.V()];
			while(!stack.isEmpty()){
				int v = stack.pop();
				if(!marked[v]){
					dfs(v);
					count++;
				}
			}
		}else{//无向图的连通分量
			for(int i = 0; i < g.V(); i++){
				if(!marked[i]){
					dfs(i);
					count++;
				}
			}
		}
	}

	private void dfs(int v){
		if(!marked[v]){
			marked[v] = true;
			id[v] = count;
			for(Edge e : g.adjEdge(v)){
				int w = e.other(v);
				dfs(w);
			}
		}
	}

	public int count(){
		return count;
	}

	//与顶点v属于同一连通分量的所有顶点
	public List<Integer> allConnected(int v){
		LinkedList<Integer> list = new LinkedList<Integer>();
		int k = id[v];
		for(int i = 0; i < g.V(); i++){
			if(id[i] == k){
				list.add(i);
			}
		}
		return list;
	}

	public static void main(String[] args) throws FileNotFoundException{
		File path = new File(System.getProperty("user.dir")).getParentFile();
		File f = new File(path,"algs4-data/tinyDG2.txt");
		FileReader fr = new FileReader(f);
		Graph graph = new Graph(fr, true, false);

		System.out.println("图的表示");
		System.out.println(graph);

		ConnectedComponents cc = new ConnectedComponents(graph);

		System.out.println("连通分量数:  " + cc.count());
		System.out.println("\n");

		System.out.println("和顶点 0 共属于同一个连通分量的顶点");
		for(int i : cc.allConnected(0)){
			System.out.printf("%-3d", i);
		}
		System.out.println("\n");

		System.out.println("和顶点 3 共属于同一个连通分量的顶点");
		for(int i : cc.allConnected(3)){
			System.out.printf("%-3d", i);
		}
		System.out.println("\n");

		System.out.println("和顶点 9 共属于同一个连通分量的顶点");
		for(int i : cc.allConnected(9)){
			System.out.printf("%-3d", i);
		}
		System.out.println("\n");

		System.out.println("和顶点 6 共属于同一个连通分量的顶点");
		for(int i : cc.allConnected(6)){
			System.out.printf("%-3d", i);
		}
		System.out.println();
	}
}

图的临接表示,包含了很多实用的方法,但是此处主要使用通过原图构造它的反方向图和逆后序

package datastruct;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Scanner;

public class Graph{
	private int v;//顶点数量
	private int e;//边数量
	private boolean isWeight;  //时候是带权重的图
	private boolean isDirect;  //是否是有向图
	private boolean hasCycle;  //图中时候含有环
	private LinkedList<Edge>[] adj;//临接表

	//图中边的表示
	public static class Edge implements Comparable<Edge>{
		private final int from;//边起始顶点
		private final int to;//边终结顶点
		private final double w; //权值
		public Edge(int from, int to, double w){
			this.from = from;
			this.to = to;
			this.w = w;
		}

		//返回任意一个顶点
		public int either(){
			return from;
		}

		//返回另一个顶点
		public int other(int v){
			return v == this.from ? to : from;
		}

		//用于有向图
		public int from(){
			return from;
		}

		//用于有向图
		public int to(){
			return to;
		}

		public double weight(){
			return w;
		}

		//边比较器,已权重为依据
		@Override
		public int compareTo(Edge that) {
			if(this.w > that.w){
				return 1;
			}else
			if(this.w < that.w){
				return -1;
			}else{
				return 0;
			}
		}

		//边的显示方法
		@Override
		public String toString(){
			return new String(String.format("%-2d", from) + "  "
							  + String.format("%-2d", to) + "  " 
					          + String.format("%-4.2f", w));
		}
	}

//	public static class Cmp implements Comparator<Edge>{
//		@Override
//		public int compare(Edge e1, Edge e2) {
//			return e1.compareTo(e2);
//		}
//	}

	//从文件流中读入图的txt文件来构造图
	@SuppressWarnings("unchecked")
	public Graph(Reader r, boolean isDirect, boolean isWeight){
		BufferedReader br = new BufferedReader(r);
		Scanner scn = new Scanner(br);
		v = scn.nextInt();
		e = scn.nextInt();
		this.isWeight = isWeight;
		this.isDirect = isDirect;

		adj = (LinkedList<Edge>[])new LinkedList[v];

		for(int i = 0; i < v; i++){
			adj[i] = new LinkedList<Edge>();
		}

		for(int i = 0; i < e; i++){
			int from = scn.nextInt();
			int to = scn.nextInt();
			double w;
			if(isWeight){
				w = scn.nextDouble();
			}else{//如果不是带权重的图,默认权重是1
				w = 1;
			}
			Edge e = new Edge(from, to, w);
			adj[from].add(e);
			if(!isDirect){
				adj[to].add(e);
			}
		}
		scn.close();
	}

	//当前图的反向图构造函数
	@SuppressWarnings("unchecked")
	private Graph(Graph g){
		v = g.V();
		e = g.E();
		this.isWeight = g.isWeight;
		this.isDirect = g.isDirect;
		hasCycle = g.hasCycle;

		adj = (LinkedList<Edge>[]) new LinkedList[v];
		for(int i = 0; i < v; i++){
			adj[i] = new LinkedList<Edge>();
		}

		for(int from = 0; from < v; from++){
			for(Edge e : g.adj[from]){
				int to = e.other(from);
				double 	w = e.weight();
				adj[to].add(new Edge(to, from, w));
			}
		}
	}

	//返回当前图的反向图
	public Graph reverse(){
		if(this.isDirect){
			return new Graph(this);
		}else{
			throw new IllegalArgumentException("Graph is not Directed");
		}
	}

	//通过添加边来构造图的构造函数
	@SuppressWarnings("unchecked")
	public Graph(int v, boolean isDirect, boolean isWeight){
		adj = (LinkedList<Edge>[])new LinkedList[v];
		for(int i = 0; i < v; i++){
			adj[i] = new LinkedList<Edge>();
		}
		this.isDirect = isDirect;
		this.isWeight = isWeight;
		this.v = v;
	}

	//添加一条边
	public void addEdge(Edge e){
		adj[e.from].add(e);
		this.e++;
		if(!isDirect){
			this.e++;
			adj[e.to()].add(e);
		}
	}

	//返回图中顶点个数
	public int V(){
		return v;
	}

	//返回图中边的数量
	public int E(){
		return e;
	}

	//邻接顶点,返回与顶点v相邻的所有顶点的编号
	public List<Integer> adjVertex(int v){
		ArrayList<Integer> list = new ArrayList<Integer>(adj[v].size());
		for(Edge e : adj[v]){
			list.add(e.other(v));
		}
		return list;
	}

	//返回与顶点v相邻的边,对于位于同一包中的类,这个方法效率更高
	public List<Edge> adjEdge(int v){
		return adj[v];
	}

	//返回一条边
	public Edge getEdge(int from, int to){
		for(Edge e : adj[from]){
			if(e.other(from) == to){
				return e;
			}
		}
		return null;
	}

	//是否是有向图
	public boolean isDirect(){
		return isDirect;
	}

	//是否是带权重的图
	public boolean isWeight(){
		return isWeight;
	}

	//是否是有向无有环图
	public boolean isDAG(){
		if(!isDirect){
			return false;
		}

		boolean[] marked = new boolean[v];
		boolean[] onStack = new boolean[v];

		for(int i = 0; i < v; i++){
			if(!marked[i]){
				dfs(i, marked, onStack);
			}
		}
		return !hasCycle;
	}

	//用于判断DAG的深度优先遍历
	private void dfs(int v, boolean[] marked, boolean[] onStack){
		if(hasCycle){
			return;
		}

		marked[v] = true;
		onStack[v] = true;
		for(Edge e : adj[v]){
			int w = e.other(v);
			if(!marked[w]){
				dfs(w, marked, onStack);
			}else
			if(onStack[w]){
				hasCycle = true;
				return;
			}
		}
		onStack[v] = false;
	}

	//图的显示方法
	public String toString(){
		StringWriter sw = new StringWriter(5*v + 10*e);//长度不是一个准确值,是尽量往大估计的
		PrintWriter pw = new PrintWriter(sw);
		for(int i = 0; i < v; i++){
			pw.printf("%-3d: ", i);
			for(Edge e : adj[i]){
				if(isWeight){
					pw.printf("[%-3d, %-4.2f]  ", e.other(i), e.w);
				}else{
					pw.printf("%-3d ", e.other(i));
				}
			}
			pw.println();
		}
		return sw.getBuffer().toString();
	}

//是否存在从from到to的边
//	public boolean hasEdge(int from, int to){
//		boolean[] marked = new boolean[v];
//		hasEdge0(from, to, marked);
//		return marked[to];
//	}
//
//	private void hasEdge0(int from, int to, boolean[] marked){
//		if(!marked[from]){
//			marked[from] = true;
//			for(Edge e : adj[from]){
//				if(!marked[to]){
//					hasEdge0(e.other(from), to, marked);
//				}else{
//					return;
//				}
//			}
//		}
//	}

	//从from节点开始逆后序遍历,返回逆后序的栈
	public LinkedList<Integer> reversePostOrder(int from){
		LinkedList<Integer> stack = new LinkedList<Integer>();
		boolean[] marked = new boolean[v];
		for(int i = 0; i < v; i++){
			reversePostOrderTar(i, stack, marked);
		}
		return stack;
	}

	//用于逆后序的深度优先遍历
	private void reversePostOrderTar(int from, LinkedList<Integer> stack, boolean[] marked){
		if(!marked[from]){
			marked[from] = true;
			for(Edge e : adj[from]){
				reversePostOrderTar(e.other(from), stack, marked);
			}
			stack.push(from);
		}
	}

	public static void main(String[] args) throws FileNotFoundException{
		File path = new File(System.getProperty("user.dir")).getParentFile();
		File f = new File(path, "algs4-data/tinyDG.txt");
		FileReader fr = new FileReader(f);
		Graph g = new Graph(fr, true, false);
		System.out.println(g.toString());
		System.out.println(g.reverse().toString());
//		System.out.println(g.hasEdge(0, 7));
	}

}

5. 参考内容

[1]. 算法(第4版)Robert Sedgewick 人民邮电出版社

时间: 2024-10-26 06:47:23

Kosaraju算法解析: 求解图的强连通分量的相关文章

Kosaraju两次深搜实现强连通分量

Kosaraju两次深搜实现强连通分量 kosaraju算法进行两次dfs,第一次在原图上进行,并在结点递归调用返回时将结点压入一个栈中,第二次dfs在原图的逆图上进行,并且初始点选择栈中最上面的点,每次dfs所访问的点构成一个强连通分量. 第一次看kosaraju算法的时候,我很不解,为什么第二次dfs随便遍历一下就能找到一个强连通分量呢?后来才顿悟,这里关键是选取的遍历起始点的顺序.这里就要好好研究一下为什么第一次遍历能够为第二次遍历打下这么神奇的基础.其实第一次dfs的操作,非常像一个基础

图的强连通分量-Kosaraju算法

输入一个有向图,计算每个节点所在强连通分量的编号,输出强连通分量的个数 1 #include<iostream> 2 #include<cstring> 3 #include<vector> 4 using namespace std; 5 const int maxn=1024; 6 struct Edge{ 7 int go,next; 8 }; 9 int vis[maxn],count=0,book[maxn]; 10 vector<Edge> G,

求图的强连通分量--tarjan算法

一:tarjan算法详解 ?思想: ? ?做一遍DFS,用dfn[i]表示编号为i的节点在DFS过程中的访问序号(也可以叫做开始时间)用low[i]表示i节点DFS过程中i的下方节点所能到达的开始时间最早的节点的开始时间.(也就是之后的深搜所能到达的最小开始时间)初始时dfn[i]=low[i] ? ?在DFS过程中会形成一搜索树.在搜索树上越先遍历到的节点,显然dfn的值就越小. ? ?DFS过程中,碰到哪个节点,就将哪个节点入栈.栈中节点只有在其所属的强连通分量已经全部求出时,才会出栈. ?

寻找图的强连通分量:tarjan算法简单理解

1.简介tarjan是一种使用深度优先遍历(DFS)来寻找有向图强连通分量的一种算法. 2.知识准备栈.有向图.强连通分量.DFS. 3.快速理解tarjan算法的运行机制提到DFS,能想到的是通过栈来储存沿途的点,可以找到所有的环.环本身就是联通的,所以环对于强连通分量来说环已经很接近最终答案了.要把找环变成找强连通管分量还要考虑:a.在环外是不是有其他环在这个强连通分量内(极大性) (会被认为是2个环) b.一些不能构成环的点无法被考虑到,而他们本身就是强连通分量 (2不被认为是一个强连通分

tarjan算法+缩点:求强连通分量 POJ 2186

强连通分量:1309. [HAOI2006]受欢迎的牛 ★★   输入文件:cow.in   输出文件:cow.out   简单对比时间限制:1 s   内存限制:128 MB [题目描述] 每一头牛的愿望就是变成一头最受欢迎的牛.现在有N头牛,给你M对整数(A,B),表示牛 A 认为牛 B受欢迎.这种关系是具有传递性的,如果A认为B受欢迎,B认为C受欢迎,那么牛A也认为牛C受欢迎.你的任务是求出有多少头牛被所有的牛认为是受欢迎的. [输入格式] 第1行两个整数N,M: 接下来M行,每行两个数A

Tarjan算法:求解图的割点与桥(割边)

简介: 割边和割点的定义仅限于无向图中.我们可以通过定义以蛮力方式求解出无向图的所有割点和割边,但这样的求解方式效率低.Tarjan提出了一种快速求解的方式,通过一次DFS就求解出图中所有的割点和割边. 欢迎探讨,如有错误敬请指正 如需转载,请注明出处 http://www.cnblogs.com/nullzx/ 1. 割点与桥(割边)的定义 在无向图中才有割边和割点的定义 割点:无向连通图中,去掉一个顶点及和它相邻的所有边,图中的连通分量数增加,则该顶点称为割点. 桥(割边):无向联通图中,去

图论算法之(强连通分量&lt;Kosaraju&gt;)

强连通分量算法有3个之多,现在介绍这种名字叫做kosaraju算法. 这个算法基于两个事实,1.原图G与逆置图GT拥有相同的强连通分量,这肯定是正确的 2.任意一个子节点存放皆后于父节点,也就是说所有只有当所有子节点都入栈了,父节点才入栈 这种在递归调用之后将顶点入队列的方式叫逆后续排序(reverse post),在无环图中这种排序方式就是拓扑排序. 简要证明: 1. 第一次DFS有向图G时,最后记录下的节点必为最后一棵生成树的根节点. 证明:假设最后记录下节点不是树根,则必存在一节点为树根,

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

有向图的强连通分量 定义:在有向图\(G\)中,如果两个顶点\(v_i,v_j\)间\((v_i>v_j)\)有一条从\(v_i\)到\(v_j\)的有向路径,同时还有一条从\(v_j\)到\(v_i\)的有向路径,则称两个顶点强连通(strongly connected).如果有向图\(G\)的每两个顶点都强连通,称\(G\)是一个强连通图.有向图的极大强连通子图,称为强连通分量(strongly connected components). 万能的Tarjan算法也可以帮助我们求解有向图的强

强联通分量之kosaraju算法

首先定义:强联通分量是有向图G=(V, E)的最大结点集合,满足该集合中的任意一对结点v和u,路径vu和uv同时存在. kosaraju算法用来寻找强联通分量.对于图G,它首先随便找个结点dfs,求出每个节点最后一次访问的时间戳f(x),然后我们建立反图Gt,接着根据倒序的时间戳来dfs每个节点,每次dfs到的结点集合就是一个强联通分量.事实上这个算法的思想和拓扑排序类似. 我们来证明它(注意这里面的图指原图,而不是反图): 引理:对于G中的两个强联通分量C和C',若点u属于C,点v属于C',且