所谓有向图,指的是图中的通路具有方向性。例如: A ---> B, 表示从节点 A 到 B 有条通路, 而从 B 到 A 是不通的。有向图中的强连通分量,是指有向图的一群节点,这些节点相互之间都有路径。 求图的强连通分量都是通过对图进行深度优先搜索来实现。深度优先搜索有一些奇妙的性质。深度优先搜索最终会将图析构成一棵树(普通意义上的树),那么这棵树上的节点发现的顺序,称为时间戳,一般的算法书上会用 DFN(x) 表示节点 x 的时间戳。一个节点有两个相关的时间戳,一个是节点被深度优先搜索第一次遍历,使用记号 d[v] 表示, 也用 dfn[v] 来表示。另外一个是以该节点作为树根的树被遍历完的时间戳,使用 f[v] 来表示。下面对图的深度优先遍历会使用以下的记法:
( A (C (D, D) (B, B) C) A)
1 2 3 4 5 6 7 8
Node(x) ( d(x), f(x))
A 1 8
C 2 7
D 3 4
B 5 6
这里的道理是显然的,其实图的深度优先搜索就是树的先序遍历,只不过在图的深度优先遍历中,不可避免的会存在回边,但是在对图的节点做访问标志标记后,可以防止访问之前访问过的节点。在图的遍历中,存在以下四种边。
1)
树枝边 (FOREST) , 例如上图 A-->C
2)
前向边 (FORWARD)
3)
后向边 (BACKWARD) 例如上图 B-->A
4)
横跨边 (CROSS)
下面说说 Gabow 算法,该算法定义了节点的如下一种性质:
LowLink(U) = min{ dfn(U), dfn(W)}; 以下简单记为
L(U) = min{ d(U), d(W)};
W 是从 U 或者 U的后代出发用一条 BACKWARD 或者 CROSS 所能到达的同意强连通分支的顶点。
由此可见, L(U) 是U所处的强分支中出发先用 FOREST , FORWARD,最后使用 BACKWARD, CROSS 能到达的 dfn 最小的顶点的序值。对于强连通分支的树根 R,显然有 L(R) = d(R), 因此,当深度搜索从 d(X) = L(X) 的顶点X返回时,从树中移去以 X为根的所有顶点,这些顶点构成一个强连通分量。
现在来计算顶点 U 的 L(U) 值。
1)
第一次访问节点 U, 做 L(U) = d(u);
2)
通过后向边 (u, w), 做 L(U) = min {LL(v), LL(w)}
3)
遇到横跨边 (u, w), 做 L(U) = min { LL(v), DFN(w)}
4)
从 U 的儿子 W 返回时, 做 L(U) = min { LL(v), LL(w)}
对于图的遍历序列,写成以下形式:
A B C D E F G H I J K
例如: L(G) = dfn(D), 那么我们可以用如下形式表示:
A B C D E F G
|_____|
也就是从G --> D 的通路是存在的。
下面证明 Gabow 算法的正确性。
必要性:
1)
从 A ----> X , X 为非根节点,这条路径是存在的。
2)
从 X ---> Y, 只要证明 X ---> A 存在,那么
X --> A ---> Y 存在,即 X -->Y 存在,这里 X 指任意一个节点。
3)
证明 X --> A 存在, 因为 L(X) < dfn(X), 那么 X --> P, (在序列中 P 在 X 前面),同理 L(P) < dfn (P),那么存在 P --> Q (在序列中 Q 在 P 前面),图形表示如下:
A B C D E F G H I J K L M
|_________| |_____| |___________| |_________|
就是说 L(M) = dfn(J)
L(J) = dfn(F)
L(F) = dfn (D)
L(D) = dfn(A)
这样一系列的路径 M --> J --> F ---> D ----> A .
算法的具体操作过程如下:
从根节点 A 出发深度优先遍历,检查的条件是当从以某一个节点为根的子树遍历完成是看 if ( L(R) == dfn(R) ),这是合理的, 可以通过模拟栈来实现。当某节点的 L(U) = dfn(U)的时候,这个节点无法访问其祖先节点,而他的所有儿子节点亦无法访问其祖先节点,因为如果有儿子能访问其祖先的话,那么根据计算法则四 L(U) 必定小于 dfn (U),因为祖先节点的 dfn 肯定要比儿子的小。反映到图上就是:
A B C D E F G H I J K
------------------| |------------------------------
从 E - K 没有一个节点能访问到 A - D, 所以 E -K 是一个单独的极大连通分量。当任何一个极大连通分量分离出来后,任何指向这个连通分量的边都应该忽略。假设这个边的狐头顶点位与另外一个极大连通分量中,如果还有一个从原来连通分量到这个连通分量的边的话,那么开始遍历的时候,这个连通分量就会被包含进去。
关于四条计算规则:
1)
肯定没有问题;
2)
没有问题
4)
没有问题
简单的看看 3), 假定序列如下:
A ( [T1] [T2] [T3] )
|____________|
为什么 CROSS 需要考虑进去? 这里 [T3] ---> K , K 属于 [T1], 要看到 [T1] 还在栈中,这说明了 [T1] A 是属于一个连通分量的。而且 [T1] 已经检查过了,而且没有拿掉,所以存在:
A ( [T1] [T2] [T3])
|_____| |_______________|
还有一个值得注意的是,检查是从任何的叶子节点慢慢向上开始了。
充分性:
使用归纳法,或者反证法,叙述起来很麻烦,想倒是很容易。
Kosaraju 算法使用 2 次 DFS , 一次是正的,一个是反的。很容易理解。对于一个真正的强连通分量,不论是正的遍历还是反的遍历,都可以把所有的节点纳入到一个树中。这一点是肯定的。该算法通过两次遍历生成 T1, T2, 然后找两者的共同元素。请注意 T1, T2 含有共同的根节点。 假定集合 P 中的顶点是T1, T2 的交集。假定 T1 是正DFS, T2 是反 DFS;
那么假定根节点为 R, 对集合的任意两点 A, B, 下面正面 A --> B 存在。
因为 A 属于 T2, 那么存在 A --> R
因为 B 属于 T1, 那么存在 R--> B, 所以存在 A -->B. 这个结论的充分性也很好证明;