Kosaraju算法
该算法旨在得到深度优先后续排列后的递归探索中,每次调用DFS的所有顶点都属于同一强联通分量。所以可以这么理解:当递归进入一个强联通分量时,把他锁死在这个强联通分量中(即不能从该强联通分量中的任意顶点达到外部顶点)
下面来说说该算法的思路,我认为这是一个十分巧妙的算法,有兴趣的同学可以去看算法导论这本书,我觉得关于这个算法的解释讲的特别好。
该算法的核心思想就是两次DFS遍历,可以先遍历原图再遍历转置图(即所有边都反向的图),也可以先遍历转置图再遍历原图。
下面我们按先遍历原图再遍历转置图来说一说该算法的大致实现流程:
- 深度优先遍历原图G(V,E),并记录每个结点遍历结束的时间(将该点的所有子孙结点都遍历完从该点退出的时间即为该点遍历的结束时间,由此可知该结点的结束时间比他的所有子孙节点的结束时间都要晚)
- 将该图逆转得到转置图(需要明白的是转置图的强连通分量和原图的强连通分量完全相同,即逆转图不会改变图的强连通分量)
- 在转置图上每次取出结束时间最晚且未遍历过的点进行深度优先遍历,并将遍历过的结点标记(下次遍历不再遍历这些被标记过的点),每一次深度优先遍历得到的点即为一个强连通分量。
那么为什么按上述方法的到的为强连通分量呢?且听我慢慢道来:
要理解kosaraju算法,首先得理解拓扑排序,即访问每个强连通分量都得有一个先后顺序,使得每次访问一个强连通分量时,都不能从该强连通分量到达其他强连通分量(即该强连通分量中若有边指向其他强连通分量,则他所指向的强连通分量一定已经被访问过了)
那么如何使得每次访问一个强连通分量时,都不能从该强连通分量中的任意顶点达到外部顶点呢?按上述所说的每次取出遍历结束时间最大的点,在转置图中进行深度优先遍历即可完美解决该问题。
我们实际上是在以拓扑排序的次序来访问分量图中的结点。(分量图:即将每个强连通分量都浓缩成一个结点得到的一个有向无环图,该图中的每一个结点对应原图G的一个强连通分量)
(结点的发现时间:即第一次访问到该结点的时间;结点的完成时间:即DFS从该结点退出时的时间,同遍历结束时间的意思相同)
这里的d(C)<d(C')指的是在第一次DFS中强连通分量C中的结点要比C'中的结点先访问(不一定C中的所有结点都比C'要先访问,但是C中一定至少存在一个结点的访问时间要比C'中的所有结点都要早);同理d(C)>d(C')表示C’中的结点要比C中的结点先访问。
根据上述定理可知,将原图逆转后的到的分量图中的边都是由 f 较小的强连通分量指向 f 较大的强连通分量。
这里的E^T是指的转置图的边。
所以当我们每次取出完成时间最晚的那个顶点进行深度优先遍历时,从该顶点所表示的强连通分量的所有出边(即指出该强连通分量的边)所指向的强连通分量一定已经访问(因为他们的 f 要大于该强连通分量的 f ,这即是拓扑排序的一个应用),所以不可能有一条边可以跳出该强连通分量,故DFS被锁死在该强连通分量中,所以该次DFS所访问的点即为该强连通分量中的所有点。
下面是代码:
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#define MAX_SIZE 100
typedef int Elemtype;
typedef struct{
int vexsnum;
int arcsnum;
Elemtype vexs[MAX_SIZE];//结点表
int arcs[MAX_SIZE][MAX_SIZE];//临接矩阵
}Mgraph;//图结构
void DFS(Mgraph G,int i,int key);//从结点i开始进行深搜
int DFStraving(Mgraph G,int key);//当key为1时,在深搜的同时打印结点
void creat_graph(Mgraph* G);//创建有向图
int get_location(Mgraph G,Elemtype v);//找到元素v在结点表中的位置
void recreat(Mgraph* G);//逆转原图
bool visited[MAX_SIZE]={false};
int finish[MAX_SIZE]={0};//记录结点访问完成的时间
int time=0;
int main()
{
Mgraph G;
creat_graph(&G);
int cnt;
DFStraving(G,0);
recreat(&G);
cnt=DFStraving(G,1);
printf("强连通分量的个数:%d\n",cnt);
return 0;
}
void DFS(Mgraph G,int i,int key)
{
visited[i]=true;
int j;
if(key==1)
printf("%d ",G.vexs[i]);
for(j=0;j<G.vexsnum;j++)
if(G.arcs[i][j]&&!visited[j])
DFS(G,j,key);
if(key==0)
finish[i]=++time;
}
int DFStraving(Mgraph G,int key)//第一次调用时key为0,为的是得到finish数组,第二次调用时key为1
{
int i,cnt=0;
if(key==0)
{
for(i=0;i<G.vexsnum;i++)
if(!visited[i])
DFS(G,i,key);
}
else if(key==1)
{
printf("强连通分量:\n");
memset(visited,0,MAX_SIZE*sizeof(bool));
while(1)
{
int max=-1;
for(i=0;i<G.vexsnum;i++)//找出完成时间最大的点
{
if(visited[i]==false&&max==-1)
max=i;
if(visited[i]==false&&max!=-1&&finish[max]<finish[i])
max=i;
}
if(max==-1)//所有点都已经被访问
break;
DFS(G,max,key);
cnt++;
printf("\n");
}
}
return cnt;
}
void creat_graph(Mgraph* G)
{
printf("请输入有向图的结点数和边数:");
scanf("%d%d",&(G->vexsnum),&(G->arcsnum));
int i;
memset(G->arcs,0,(MAX_SIZE)*(G->vexsnum)*sizeof(int));//初始化邻接矩阵
printf("请输入各结点的值:");
for(i=0;i<G->vexsnum;i++)
scanf("%d",&(G->vexs[i]));
printf("请输入有向边所依附的两结点的值(从结点1指向结点2):");
Elemtype m,n;
int x,y;
for(i=0;i<G->arcsnum;i++)
{
scanf("%d%d",&m,&n);
x=get_location(*G,m);
y=get_location(*G,n);
G->arcs[x][y]=1;
}
printf("有向图创建完成!\n");
}
int get_location(Mgraph G,Elemtype v)
{
int i;
for(i=0;i<G.vexsnum&&G.vexs[i]!=v;i++);
return i;
}
void recreat(Mgraph* G)
{
int i,j,e;
for(i=0;i<G->vexsnum;i++)
for(j=i;j<G->vexsnum;j++)//这里很细节,不能从j=0开始,否则会有重复,做使边做两次交换,相当于没有对图做逆转
{
if(G->arcs[i][j]!=G->arcs[j][i])
{
e=G->arcs[i][j];
G->arcs[i][j]=G->arcs[j][i];
G->arcs[j][i]=e;
}
}
}