引言
本文我们着重分析下有向图的强连通性以及如何计算有向图中的强连通分量。
有关图的概念可参考博文数据结构之图的概述
强连通
在一幅无向图中,如果有一条路径连接顶点v
和w
,则它们就是连通的;然后,在一幅有向图中,如果从顶点v
有一条有向路径达到w
,则顶点w
是从顶点v
可达的,但如果从w
到达v
的路径可能不存在。这两个顶点不是强连通的。
如果两个顶点互相可达,则它们是强连通的。如果一幅有向图中任意两个顶点都是强连通的,则这幅有向图也是强连通的。
强连通分量
有向图中的强连通性是一种顶点之间平等关系,有着以下性质:
- 自反性: 任意顶点和自己都是强连通的
- 对称性:如果
v
和w
是强连通的,那么w
和v
也是强连通的 - 传递性:如果
v
和w
是强连通的且如果w
和x
也是强连通的,那么v
和x
也是强连通的
强连通性将所有顶点分成了一些平等的部分,每个部分都是由相互均为强连通的顶点的最大子集组成的。这些子集称为强连通分量(强连通分支)
如上图可以分为5个强连通分量。 一个含有V个顶点的有向图可能有1~V个强连通分量;一个强连通图只含一个强连通分量;而一个有向无环图中含有V个强连通分量。
那么如何通过代码找到有向图中的强连通分量呢?
有向图的强连通分量和无向图的连通分量不同,因为有向图带有方向,情况略为复杂。
比如考虑上图,
{
0
,
1
,
2
}
\{0,1,2\}
{0,1,2}和
{
4
,
5
,
6
,
7
}
\{4,5,6,7\}
{4,5,6,7}是两个不同的强连通分量。但是从4有一条路径到另个一分量中的2,同时6也能到8,但是反过来2却不能到4,所以2和4属于不同的分量之中。
下面一起来学习下一种计算有向图强连通分量的算法
Kosaraju算法
强烈建议先了解下无向图的连通分量计算算法
该算法看起来很简单,但是要理解为什么起作用需要仔细考虑下,算法过程如下:
- 在给定的一幅有向图
G
G
G中,使用
DepthFirstOrder
来计算它的反向图 G R G^R GR的逆后序序列。 - 在有向图 G G G中(注意是在原图中操作)按照上步得到的序列进行标准的深度优先搜索。
- 在构造函数中,所有在同一个递归
dfs()
调用中被访问到的顶点都在同一个强连通分量中
DepthFirstOrder
算法描述见图论算法——环和有向无环图
实现起来真的很简单,反向图即图中所有路径方向逆转后的图。
关键是为啥有效?
我们一步一步来分析。
看算法过程,为什么要引入 G R G^R GR,它是干嘛的?
G R G^R GR是正向图的反向图,它和正向图有同样的强连通划分。什么?不信!
上图是我们要计算强连通分量的有向图,嗯,正向图来的。
通过肉眼可以看到有5个强连通分量,我通过不同颜色的矩形框起来了。下面给出它的反向图:
同样,通过肉眼可以观察到所有路径的方向都逆转了,但是分量和正向图的分量一样。
下面继续分析为什么用逆后序,最后才能得出为什么要计算反向图的逆后序序列。
回顾我们之前对逆后序的探讨,在对 s s s做深度优先搜索,我们发现可以访问 v v v,那么必然有 s → v s \rightarrow v s→v
dfs(DiGraph g,int s) {
...
dfs(g,s);
...
dfs(g,v);
...
v done;
...
s done;
...
}
伪代码如上所示,
v
v
v先入栈,
s
s
s会在它之后入栈。根据栈的后进先出特性,最后得到的顺序为:
s
s
s,
v
v
v 。
所以,如果逆后序序列为:
s
s
s,
v
v
v 我们可以猜测为
s
→
v
s \rightarrow v
s→v。
为什么说是猜测呢?我们以上面正向图的实例来解析,得到它的逆后序遍历顺序为:
4
,
5
,
7
,
6
,
3
,
8
,
9
,
0
,
1
,
2
4,5,7,6,3,8,9,0,1,2
4,5,7,6,3,8,9,0,1,2
首先我们看 4 , 5 , 7 , 6 4,5,7,6 4,5,7,6,诶,这个顺序没问题, 4 → 5 → 7 → 6 4 \rightarrow 5 \rightarrow 7 \rightarrow 6 4→5→7→6 继续往后走, 6 , 3 6,3 6,3,但是它们之间就没有路径了。只能说明 6 6 6在 3 3 3前面被访问。
回顾下无向图的计算连通分量算法,假设我们先访问4
,接下来可以选择5
或6
,假设先选择5
吧,然后访问7
和6
。6
可以访问4
和8
,4
因为之前已经访问过(marked[]
数组标记过),所以不会再次访问,但是如果访问8
就会跑到另外一个连通分量去了。
这时,脑海里有没有浮起:如果8
已经访问过就好了。
现在,再次关注反向图。
我们可以发现,在反向图中,6
访问不到8
,同时4
也访问不到2
了。这时可能有同学会说,那直接访问反向图就好了。非也,相反地,2
也是能访问4
的。也会存在同样的问题,只不过换了对象而已。
好了,到了解答这个问题的时候了
为什么要计算反向图的逆后序序列?
回到正向图的访问顺序,如存在一个这样的顺序,使得2
比4
先访问,8
比6
先访问,那么依次
{
4
,
5
,
7
,
6
}
\{4,5,7,6\}
{4,5,7,6} 得到的就是强连通分量。
没错,反向图的逆后序序列就是我们想要的顺序。以上图为例,得出它的反向图的逆后序序列为: 9 , 8 , 3 , 0 , 2 , 4 , 6 , 7 , 5 , 1 9,8 ,3, 0, 2, 4, 6, 7, 5, 1 9,8,3,0,2,4,6,7,5,1
我们根据这个序列对正向图
G
G
G进行dfs
访问:
- 先访问
9
,9
没有邻接点,递归调用dfs(9)
结束,得到分量 { 9 } \{9\} {9} - 访问
8
,8
也没有邻接点,得到分量 { 8 } \{8\} {8} - 访问
3
,3
有邻接点8
,但是8
刚才已经访问过了(^_^
),递归调用dfs(3)
结束,得到分量 { 3 } \{3\} {3} - 访问
0
,按照0,1,2
的顺序访问到2
,2
有一个邻接点0
,已经访问过了。递归调用dfs(0)
结束,得到分量 { 0 , 1 , 2 } \{0,1,2\} {0,1,2} - 访问
2
,它已经访问过了 - 访问
4
,4
的邻接点有2,5
,但是2
已经访问过了,然后依次访问5,7,6
,6
接下来想尝试访问8
,但是8
早已被访问。得到分量 { 4 , 5 , 7 , 6 } \{4,5,7,6\} {4,5,7,6} - 反向图的逆后序序列剩下的
6,7,5,1
都已经被访问,就不细说了
最后就得到了5个强连通分量。
本文的分量都是对强连通分量的简称
接下来看一下代码实现
Kosaraju算法实现
本文相关代码实现涉及到栈和队列的请访问:栈和队列的实现
package com.algorithms.graph;
import com.algorithms.queue.Queue;
/**
* 计算有向图的强连通分量
* @author yjw
* @date 2019/5/22/022
*/
public class KosarajuSCC {
private boolean[] marked;
/**
* 标识连通分量,同一连通分量的值相同
* 0:第一个连通分量
* 1:第二个连通分量
* ...
*
* 值为0到count - 1之间
*/
private int[] id;
/**
* 连通分量数
*/
private int count;
public KosarajuSCC(DiGraph g) {
marked = new boolean[g.vertexNum()];
id = new int[g.vertexNum()];
DepthFirstOrder order = new DepthFirstOrder(g.reverse());
for (int s : order.reversePost()) {
if (!marked[s]) {
dfs(g,s);
count++;
}
}
}
private void dfs(DiGraph g,int v) {
marked[v] = true;
id[v] = count;//标识连通分量
for (int w: g.adj(v)) {
if (!marked[w]) {
dfs(g,w);
}
}
}
public boolean stronglyConnected(int v,int w) {
return id[v] == id[w];
}
public int id(int v) {
return id[v];
}
public int count() {
return count;
}
@SuppressWarnings("unchecked")
public void print() {
System.out.println(count + " components");//count个连通分量
Queue<Integer>[]components = (Queue<Integer>[]) new Queue[count];
for (int i = 0; i < components.length; i++) {
components[i] = new Queue<>();
}
for (int i = 0; i < id.length; i++) {
components[id(i)].enqueue(i);
}
for (Queue<Integer> queue : components) {
System.out.println(queue);
}
}
public static void main(String[] args) {
DiGraph g = new DiGraph(10);
g.addEdge(0,1,1,2,2,0);
g.addEdge(4,2,4,5,5,7,7,6,6,4,6,8);
g.addEdge(8,9,3,8);
//System.out.println(g);
KosarajuSCC cc = new KosarajuSCC(g);
cc.print();
}
}
和无向图的连通分量计算算法很像,除了dfs
调用的顺序不同。再次建议先了解下无向图的连通分量计算算法
执行上面的主函数,输出如下:
5 components
[9]
[8]
[3]
[0 1 2]
[4 5 6 7]
为啥是4,5,6,7而不是上面的4,5,7,6 提这个问的同学看得很仔细,是4,5,6,7的原因请看print()
方法。
还有啥问题的欢迎在下面评论指出