有向图的强连通分量的求解是个经典问题,但是如果不深刻算法的道理,很难自己实现出来。
首先解释下算法的道理。
1、什么是强连通分量?书上定义是其中任意两个点可以相互到达的一个顶点集合。如果形象化的描述,强连通是这样的一种点的集合,这些顶点是由若个个环扩展来的(想象下图形),比如一个环A→B→C→A,然后再环的基础上加一条起点与终点都在环上的路径(如B→E→F→C),那么这个图形仍然是强联通的,用这种方法一直扩展到找不到这样的路径了,就找到了一个极大强连通分量。
2、怎么在强联通分量的基础上理解图?我们知道了强连通分量的直观理解了,如何在此基础上组成图呢,显然如果图包含了多个强连通分量,那么把这些强联通分量看成一个点,那么这些点肯定不能组成环(否则就连通起来了),不能有环的图等价于一个拓扑图(拓扑图的定义省略了)。总而言之,图就是由多多个强连通分量组成的拓扑序列。
3、什么是图的逆转?图的逆转就是把图中所以边的方向逆转。在理解前两点的基础上,我们知道把一个图逆转后,他的强连通分量是不变的。为什么?因为对于环来说只是换了个旋转方向,对于拓扑序列(由强联通分量之间组成)来说,逆向也不会产生环。
4、为什么深度优先遍历会体现图的结构?书上总是说深度优先遍历能体现图的结构,怎么体现的呢,对于有向图来说,体现的是顶点的拓扑排序。在深度优先遍历过程中有一个回溯过程,每次回溯的时候是当前顶点结束访问的时候。我们还是以强联通分量组成的拓扑系列才思考图,假设有三个强连通分量C1 C2 C3,他们的拓扑排序为C1→C2→C3,假设我们从C2中的一个顶点开始深度优先遍历,那么在C3所有顶点都结束访问后,C2才有可能结束所有点的访问,同理只有C2所有顶点结束访问后C1才有可能结束所有顶点的访问。很容易推理出最后一个结束访问的顶点一定属于C1连通分量。所以每个点的结束访问时间,如果逆序排列,可以几乎模拟拓扑排序中的顶点顺序。
注意这儿用的是几乎,因为逆序排列后不是严格按照(C1所有顶点)然后(C2所有顶点)然后(C3顶点)这样的顺序,而是把排列后的序列中去掉所有C1中的点,下一个点一定属于C2,同理再去点C2中所有点,下一个一定是C3中的(这个有点废话了 毕竟只有3个分量 哈哈)。
5、为什么算法把图逆转? 之前说了逆转后连通分量不变,还有一个好处是,逆转后如果按照原图的拓扑顺序去深度优先遍历,那么各个连通分量间就不会相互跨越访问!
如上面的例子,把C1→C2→C3逆转后变为C1←C2←C3,再按照原先的顺序深度遍历,那么遍历过程中一定不会从C1跨越到C2了,这个时候每次遍历到的点的集合就是一个强连通分量。
解释完了,理解了上面5点,算法也就很容易理解了。
算法:
1、第一次深度优先遍历图,标记每个顶点的结束访问时刻。
2、按照每个顶点的结束访问时刻逆序排列,
3、依排列顺序,选择一个未被访问的顶点深度优先遍历图。
4、如果图还有未被访问的顶点,连通分量加一,返回第3步。
下面是实现代码:
#include <iostream>
using namespace std;
#define N 10
int Vertex[N]={0};//保存顶点信息
int Arc[N][N]={0};//保存边的信息 这儿使用的是邻接矩阵图
int Visited[N]={0};//给第一个深度遍历用
int FinishedTime[N]={0};//保存每个顶点访问完成的时刻
int Visited1[N]={0};//给第二个深度遍历用 算法结束后 里面对应的是每个顶点属于的连通分量编号
int cnt=1;//计数用
int Level=1;//用来计算属于的连通分量编号
void DFS1(int a);
void DFS2(int a);
void Reverse();
int FindMax();
void show();
void main()
{
while(cin)//初始化图的信息 这儿设定图有10个顶点
{
cout<<"请输入边:\n";
int a,b;
cin>>a>>b;
if(cin)
Arc[a][b]=1;
}
for(int i=0;i!=N;++i)//第一次深度优先遍历 标记顶点完成时间
{
if(Visited[i]==0)
DFS1(i);
}
Reverse();//图的边求逆
while(cnt>1)
{
int t=FindMax();
DFS2(t);
Level++;
}
show();
::system("pause");
}
void DFS1(int a)
{
Visited[a]=1;
for(int i=0;i!=N;++i)
{
if(Visited[i]==0&&Arc[a][i]==1)
DFS1(i);
}
FinishedTime[a]=cnt++;
}
void DFS2(int a)
{
Visited1[a]=Level;
cnt--;//访问节点计数
for(int i=0;i!=N;++i)
{
if(Visited1[i]==0&&Arc[a][i]==1)
{
DFS2(i);
}
}
}
void Reverse()
{
for(int i=0;i!=N;++i)
for(int j=0;j<i;++j)
{
int t=Arc[i][j];
Arc[i][j]=Arc[j][i];
Arc[j][i]=t;
}
}
int FindMax()
{
int max=-1;int position=-1;
for(int i=0;i!=N;++i)
{
if(FinishedTime[i]>max&&Visited1[i]==0)
{
max=FinishedTime[i];position=i;
}
}
return position;
}
void show()
{
for(int i=1;i!=Level;++i)
{
cout<<"连通分量"<<i<<":\n";
for(int j=0;j!=N;++j)
{
if(Visited1[j]==i)
cout<<j<<" ";
}
cout<<endl;
}
}
测试输出: