图论中最重要的结构,很多图论问题都可以转化为强连通分支来降低处理复杂度。一个强连通分支中所有的点都是互相连通的,可以将其收缩为单个点,以此来简化图的处理。强连通分支中的点集合是一个最大集合,即再加入任何一个其他点都会导致不连通。
引理1:图G的两个强连通分支C、C’,如果存在点u属于C,u’属于C’,使得(u,u’)为G的一条边,则一定不存在另一条边(v’,v),使得v’属于C’,v属于C。
反证法,假设存在这样的一条边(v’,v),则强连通分支C和C’是互相连通的,即属于一个强连通分支,矛盾。由此可以有分支图的概念
定义:图G的分支图GSCC=(VSCC,ESCC),将图G的每一个强连通分支看作一个点,这个点属于VSCC,即分支图GSCC的点数为图G的强连通分支数目,不妨设G的强连通分支为C1,C2,...,Ck,则分支图的点集GSCC为v1,v2,...,vk;定义边(vi,vj)属于ESCC,当且仅当G中有一条边(x,y),且x属于Ci,y属于Cj,由引理1保证此边的方向是唯一的。
引理2:图G的分支图GSCC是一个有向无环图(DAG)
反证法,与引理1证明类似,假设存在一个环,则这个环上的所有的强连通分支均互相可达,与多个强连通分支矛盾。故分支图GSCC是一个有向无环图
习题22.5-1 增加一条边对图G的强连通分支个数会有什么影响?
假设增加一条边(u,v),u属于强连通分支Ci,v属于强连通分支Cj,
(1) 如果i==j,即同属于一个强连通分支,则添加这条边对强连通分支个数没有影响;
(2) 如果Cj到Ci没有路径,则添加这条边后对强连通分支个数也没有影响
(3) 如果Cj到Ci存在一条路径,则添加这条边后使用Cj,Ci,...Cj形成一个环,在这个环上的所有强连通分支会变成一个新的连通分支,总的强连通分支个数会减少。
定义:图G的转置图GT,将图G中的所有边的方向转向即成GT,严格定义G=(V,E),GT=(V,ET), ET={(u,v):(v,u)属于E}
引理3:图GT与G有着相同的强连通分支
假设图G的一个强连通分支Ci,图GT中Ci对应的CiT,此子图仍然是互相连通的,同时也是一个极大的连通子图,否则如果存在一个点u,使得CiT 与u互相连通,则在图G中Ci与u也是互相连通的,矛盾。
由此证明图GT与G的强连通分支相同。
习题22.5-4 证明((GT)SCC)T=GSCC
由引理3GT与G的强连通分支相同,则((GT)SCC)T与GSCC有着相同的点集,只要证明其边集也相同即可。
假设边(x,y)属于GSCC,即G存在两个强连通分支Ci,Cj,x属于Ci,y属于Cj,则可知(y,x)必属于(GT)SCC,于是(x,y)必属于((GT)SCC)T
假设边(x,y)属于((GT)SCC)T,则可知(y,x)必属于(GT)SCC,则(x,y)必属于GSCC
引理4:任何一个强连通分支必定包含于图G的DFS过程中的某一个子树。
证明,一个强连通分支C,在图G的DFS过程中,假设强连通分支C中第一个访问的点是u,则C中其他点均可以从u可达,根据白色路径定理,则C中其余的任何点都是点u的子孙结点,如此以u为树根的子树中一定包含该连通分支C
由引理4可知,从任意结点开始DFS,都会使得任何一个强连通分支必定全部包含于某一个DFS树中,即一个DFS树必定是由若干个强连通分支组成的。同时此引理也是强连通分支Tarjan 算法、Kosaraju的基础
DFS过程中有结点的结束时间f(v),这里定义结点集合的结束时间f(U),
f(U) = max{f(u): u属于U},即结点集合中最后完成DFS的结点时间。
引理5:图G的任意两个强连通分支C,C’,如果存在一条边(u,v),u属于C,v属于C’,则f(C) > f(C’)
证明:从C和C’这两个集合中第一个访问的结点来考虑
(1) 第一个访问的结点x属于C,则根据白色路径定理,C和C’中其余结点均是u的子孙结点,故结点x最后结束,显然有f(C)> f(C’)
(2) 第一个访问的结点x属于C’,从x必定无法访问到C,所以C’的其余结点均是x的子孙结点f(C’) =f(x),当结点x访问结点时,C中的结点尚未被访问过,故显然有f(C) > f(C’)
这个引理可以看作是有向无环图中拓扑序列的一个性质,即有向无环图存在一条边(u,v),则必须有f(u) > f(v),只要将强连通分支收缩成一个点,即将图G看作其分支图GSCC
对于其转置图GT来讲,结论正好相反,如果存在边(u,v)属于GT,且u、v分属于两个强连通分支C、C’,则有f(C) < f(C’)。
Kosaraju算法:第一次DFS获取图G的一个拓扑排序,然后按照拓扑排序的顺序,对图GT进行一次DFS,获得的DFS森林即是不同的强连通分支
此算法需要两次DFS,以下所有的f均针对第一次DFS过程。使用数学归纳法进行证明,假设第二次DFS时前k个DFS树均是强连通分支,在k=0时显然。
由归纳假设,前k个DFS树均是强连通分支,由第二次DFS过程,取余下所有点中的最晚结束的结点u,假设u属于强连通分支C,则对于尚未访问的任何一下其他连通分支C’,有f(u) = f(C) > f(C’),由引理5可知,不存在从C到C’的边,于是从访问u开始,DFS过程访问完C后就结束,恰好是一个完整的强连通分支C
将G看作GSCC再来理解Kosaraju算法,第一次DFS相当于将GSCC作了一次拓扑排序,因为GSCC是有向无环图。然后第二次DFS时,按照GSCC的拓扑逆序对GT进行DFS,相当于以(GT)SCC逆拓扑排序的方式进行DFS,所以每一个DFS树均对应(GT)SCC的一个点集,即G的一个强连通分支,又因为((GT)SCC)T= GSCC,第二次DFS正好是GSCC的拓扑排序,这是Kosaraju算法的一个隐藏性质。
这个算法初看起来甚是神奇,简单的两次DFS就可以得到强连通分支,几乎不用其他的数据结构,感谢Kosaraju这个印度人。
最后是代码实现:
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>
#include "list.h" /* list from Linux_kernel */
#include "graph.h"
static void print_scc(struct list_head *head, int scc_no)
{
struct link_vertex *v = NULL;
printf("The %d SCC:\n", scc_no);
list_for_each_entry(v, head, qnode) {
printf("%d ", v->vindex);
}
printf("\n");
}
int find_scc(struct link_graph *G)
{
int *color, i = 0;
struct link_graph GT;
struct link_vertex *v = NULL;
struct list_head topo_head, scc_head;
color = malloc(sizeof(int) * G->vcount);
for (i = 0;i < G->vcount;i++) {
color[i] = COLOR_WHITE;
}
INIT_LIST_HEAD(&topo_head);
graph_topo_sort(G, &topo_head);
printf("\n\nOutput all components of this graph\n\n");
graph_transpos(G, >);
i = 0;
list_for_each_entry(v, &topo_head, qnode) {
INIT_LIST_HEAD(&scc_head);
if (color[v->vindex] == COLOR_WHITE) {
DFS_visit_topo(>, GT.v + v->vindex, color, &scc_head);
print_scc(&scc_head, i++);
}
}
link_graph_exit(>);
free(color);
printf("This graph has %d scc components\n\n", i);
return 0;
}