这个算法用来寻找一个图的强连通子图,分为两步:
1、找到该图的反向图的一个拓扑排序
2、以该排序为顺序,深度优先遍历原图的每个节点,每个未遍历过的节点的dfs树都是一个scc(strongly connected component)。
这个算法从直观上是很简单的,如下图:
这就是一个图,其节点从左到右已经按拓扑排序,当然这个排序不是真的拓扑排序。现在先忽略两个红色的边,那这个图就是一个按严格拓扑排序的。
对于严格的拓扑,如果以从右往左的顺序dfs遍历,每一次遍历得到一个强连通子图,即一个节点,这不用证明也能看出来。
但如果这样的拓扑加入了反向的边(红色的)的话,我们惊讶地发现每次遍历得到的,咳,还是强连通子图。dfs(4)得到4,3,7的scc,dfs(2)得到2,1,0的scc。
这只是直观上的感受,试着想一下,没有反向边时从左往右是拓扑有序的,现在加入一条反向边,比如2→1,那么我们认为一定有一条正向边可以从1到达2,达成一个强连通图(其实就是个回路),如果没有的话,1→2就没有拓扑的偏序关系,那么2可以放在1左边,该反向边就不是一条反向边,矛盾,因此按反向图的拓扑对正向图dfs就能得到连通子图。注意这不是严格的证明。
一个图的拓扑可以用dfs的时间戳得到,具体用的时候就是dfs的时候用了个栈把被递归的节点存下来,这样可以求出反向图的拓扑,用这个顺序来dfs正向图即可,每次有未遍历的节点都代表一个新的子图。简单的实现一下:
#include<iostream>
#include<stack>
#include<cstring>
#include<vector>
using namespace std;
const int N=6;
int scc[N],vis[N],sec=0;
vector<int> adj[N],radj[N];
vector<int> topo;
void init()
{
for(int i=0;i<N;++i)
for(int j=0;j<N;++j)
{
int tmp; cin>>tmp;
if(tmp) {
adj[i].push_back(j);
radj[j].push_back(i);}
}
}
void rdfs(int k)
{
vis[k]=1;
for(int i=0;i<radj[k].size();++i)
if(!vis[radj[k][i]]) rdfs(radj[k][i]);
topo.push_back(k);
}
void dfs(int k)
{
vis[k]=1; scc[k]=sec;
for(int i=0;i<adj[k].size();++i)
if(!vis[adj[k][i]]) dfs(adj[k][i]);
}
void kosaraju()
{
init();
memset(vis,0,sizeof(vis));
for(int i=0;i<N;++i)
if(!vis[i]) rdfs(i);
memset(vis,0,sizeof(vis));
for(int t,i=topo.size()-1;i>=0;--i)
if(!vis[t=topo[i]])
{ ++sec; dfs(t) ; }
for(int i=0;i<N;++i)cout<<scc[i]<<" ";
}
int main(){ kosaraju(); return 0;}
输入6个点的图的边(0表示不连通,非零连通),输出为每个点所属的强连通图的标号。