求解强连通分量算法之---Kosaraju算法

本文提纲:

  • 问题描述
  • Kosaraju 算法


问题描述:

什么是强连通分量(StronglyConnected Component)(或者,被称为强连通子图,Strongly Connected Subgraph)?

首先需要明白的是,强连通分量只可能存在于有向图中,无向图中是不存在强连通分量的,当然,无向图中也有对应物,被称为连通分量(Connected Component),求解无向图中的连通分量,根据具体要求,可以选择使用并查集或者DFS


看一张取自wiki的图就明白什么是强连通分量了:



以上用虚线围绕的部分就是一个强连通分量,因此上图中总共含有三个。

对于一个强连通分量中的任意一对顶点(uv),都能够保证分量中存在路径使得u->vv->u

比如上图中由abe这三个顶点构成的分量中,任意两个顶点间都存在路径可达。

 

顺便也介绍一下有关“缩点”的概念:

由于强连通分量的特殊性,在一些实际应用中,会将每个强连通分量看成一个点,然后进行处理。这样做主要是为了降低图的复杂度,特别是在强连通分量规模大、数量多的情况中,利用“缩点”能大幅度降低图的复杂度。

缩点后得到的图,必定是DAG。用反正能够很方便的进行证明:因为若图中含有环路,即意味着至少有两个点彼此可达,那么按照强连通分量的定义,这两个点应该属于一个分量中,因而在缩点发生后,会被一个点所代表。由此推导出矛盾。比如,对上图进行缩点处理,最后的结果就是:

(abc) -> a'(fg) -> b'(cdh) -> c'

因此最后的图就可以表示为:


更加具体,更加严谨的表述,可以参考:

http://en.wikipedia.org/wiki/Strongly_connected_component


Kosaraju 算法

首先摘录一段wiki上的算法过程描述:

(取自http://en.wikipedia.org/wiki/Kosaraju%27s_algorithm)

  1. Let G be a directed graph and S be an empty stack.
  2. While S does not contain all vertices:
    1. Choose an arbitrary vertex v not in S. Perform a depth-first search starting at v. Each time that depth-first search finishes expanding a vertex u, push u onto S.
  1. Reverse the directions of all arcs to obtain the transpose graph.
  1. While S is nonempty:
    1. Pop the top vertex v from S. Perform a depth-first search starting at v. The set of visited vertices will give the strongly connected component containing v; record this and remove all these vertices from the graph G and the stack S. Equivalently, breadth-first search (BFS) can be used instead of depth-first search.

仔细观察步骤2-a,可以发现,这个过程和使用基于DFS的拓扑排序中添加顶点到最终结果栈中的过程几乎一致。(请参考这里)只不过这里的图不一定是DAG,而拓扑排序中的图一定是DAG

在这里顺便提一下在调用dfs的过程中,几种添加顶点到集合的顺序。一共有四种顺序:

  • Pre-Order,在递归调用dfs之前将当前顶点添加到queue
  • Reverse Pre-Order,在递归调用dfs之前将当前顶点添加到stack
  • Post-Order,在递归调用dfs之后将当前顶点添加到queue
  • Reverse Post-Order,在递归调用dfs之后将当前顶点添加到stack

最后一种的用途最广,至少目前看来是这样,比如步骤2-a以及拓扑排序中,都是利用的Reverse Post-Order来获取顶点集合。

 

对于DAG,利用Reverse Post-Order最终获取的集合就代表对该图进行拓扑排序的结果,但是对于非DAG而言,这样处理之后得到的是一种“伪拓扑排序”结果,为什么这样说,因为最终的结果不一定满足拓扑排序中严格的偏序定义,比如对文中第一幅图进行“伪拓扑排序“后的结果为可能为(结果不唯一,具体可以参考拓扑排序那篇文章中的相关部分)

(abecdhgf)

将结果和原图对比,可以发现,结果不满足偏序关系,因为存在回向边:

e->ad->ch->df->g

 

而这些回向边,就是构成强连通分量的关键。为了突出这些回向边,Kosaraju算法的步骤三就是将图进行转置。转置后,原来的回向边就都变成正向边了:

a->ec->dd->hg->f

对转置后的图按照上面”伪拓扑排序“中顶点出现的顺序(aebcdhgf)调用DFS,即步骤4

而每次调用DFS形成的一颗搜索树,就构成了原图中的一个强连通分量。比如调用dfs(a),会调用dfs(e),紧接着后者有调用了dfs(b)。然后依次返回,因此abe就构成了一个强连通分量。依次类推,cdh以及gf也分别构成强连通分量。

 

回顾一下Kosaraju的主要步骤:

  1. G求解Reverse Post-Order,即上文中的”伪拓扑排序“
  2. G进行转置得到GR
  3. 按照第一步得到的集合中顶点出现的顺序,对GR调用DFS得到若干颗搜索树
  4. 每一颗搜索树就代表了一个强连通分量

 

坦率地说,这个算法的想法很巧妙,为了突出回向边,而对图进行转置,然后对转置的图按照之前得到的顶点序列进行DFS调用。整个算法的确能够正确工作,但是总感觉怪怪的,确实,这个算法不太好理解,尽管它的实现十分简单直观。

代码:

#ifndef __KOSARAJU_SCC_H__
#define __KOSARAJU_SCC_H__

#include "Digraph.h"
#include "DepthFirstOrder.h"

class KosarajuSCC {
private:
	bool* marked;		// 已经访问过的顶点
	int* id;			// 强连通分量的标识符
	int count;			// 强连通分量的个数

public:
	KosarajuSCC(Digraph G);
	~KosarajuSCC() { delete[] marked; delete[] id; }

	bool stronglyConnected(int v, int w)const { return id[v] == id[w]; }
	int getId(int v)const { return id[v]; }
	int getCount()const { return count; }

private:
	void dfs(Digraph G, int v);
};

KosarajuSCC::KosarajuSCC(Digraph G) {
	count = 0;
	int vNum = G.getV();
	marked = new bool[vNum];
	id = new int[vNum];
	for (int i = 0; i < vNum; ++i) {
		marked[i] = false;
		id[i] = i;
	}
	// 对反向图进行DFS
	//DepthFirstOrder order(G.reverse());
	// 得到逆后序
	//std::stack<int> stk(order.getReversePost());
	// 在原图中对逆后序排列进行dfs
	//while (!stk.empty()) {
	//	if (!marked[stk.top()]) {
	//		dfs(G, stk.top());
	//		++count;
	//	}
	//	stk.pop();
	//}

	// 第二种做法
	// 对原图进行DFS
	DepthFirstOrder order(G);
	// 得到逆后序
	std::stack<int> stk(order.getReversePost());
	// 获得反向图
	Digraph R = G.reverse();
	// 在反向图中对逆后序排列进行dfs
	while (!stk.empty()) {
		if (!marked[stk.top()]) {
			dfs(R, stk.top());
			++count;
		}
		stk.pop();
	}
}

void KosarajuSCC::dfs(Digraph G, int v) {
	marked[v] = true;
	id[v] = count;
	for (int w : G.getAdj(v)) {
		if (!marked[w])
			dfs(G, w);
	}
}


#endif

下面对这个算法的正确性进行证明:(如果没有兴趣,可以直接略过 D)

证明的目标,就是最后一步 --- 每一颗搜索树代表的就是一个强连通分量

证明:设在图GR中,调用DFS(s)能够到达顶点v,那么顶点sv是强连通的。

两个顶点如果是强连通的,那么彼此之间都有一条路径可达,因为DFS(s)能够达到顶点v,因此从sv的路径必然存在。现在关键就是需要证明在GR中从vs也是存在一条路径的,也就是要证明在G中存在sv的一条路径。

而之所以DFS(s)能够在DFS(v)之前被调用,是因为在对G获取ReversePost-Order序列时,s出现在v之前,这也就意味着,v是在s之前加入该序列的(因为该序列使用栈作为数据结构,先加入的反而会在序列的后面)。因此根据DFS调用的递归性质,DFS(v)应该在DFS(s)之前返回,而有两种情形满足该条件:

  1. DFS(v) START -> DFS(v) END -> DFS(s) START -> DFS(s) END
  1. DFS(s) START -> DFS(v) START -> DFS(v) END -> DFS(s) END

是因为而根据目前的已知条件,GR中存在一条sv的路径,即意味着G中存在一条vs的路径,而在第一种情形下,调用DFS(v)却没能在它返回前递归调用DFS(s),这是和G中存在vs的路径相矛盾的,因此不可取。故情形二为唯一符合逻辑的调用过程。而根据DFS(s) START -> DFS(v) START可以推导出从sv存在一条路径。

所以从sv以及vs都有路径可达,证明完毕。

 

复杂度分析:

根据上面总结的Kosaraju算法关键步骤,不难得出,该算法需要对图进行两次DFS,以及一次图的转置。所以复杂度为O(V+E)


来自:http://blog.csdn.net/dm_vincent/article/details/8554244
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值