对于一个有向图顶点的子集S,如果在S内取两个顶点u和v,都能找到一条从u到v的路径,那么就称S是强连通的。如果在强连通的顶点集合S中加入其他任意顶点集合后,它都不再是强连通的,那么就称S是原图的一个强连通分量(SCC:strongly connected component)。任意有向图都可以分解成若不相干的强连通分量,这就是强连通分量分解。把分解后的强连通分量缩成一个顶点,就得到了一个DAG(有向无环图)。
强连通分量分解可以通过两次简单的DFS实现。第一次DFS时,选取任意顶点作为起点,遍历所有尚未访问过的顶点,并在回溯前给顶点标号(post order,后序遍历)。对剩余的未访问过的顶点,不断重复上述过程。
完成标号后,越接近图的尾部(搜索树的叶子),顶点的标号越小。第二次DFS时,先将所有边反向,然后以标号最大的顶点为起点进行DFS。这样DFS所遍历的顶点集合就构成了一个强连通分量。之后,只要还有尚未访问的顶点,就从中选取标号最大的顶点不断重复上述过程。
可以将强连通分量缩点并得到DAG。此时可以发现,标号最大的节点就属于DAG头部(搜索树的根)的强连通分量。因此,将边反向后,就不能沿边访问到这个强连通分量以外的顶点。而对于强连通分量内的其他顶点,其可达性不受边反向后的影响,因此在第二次DFS时,可以遍历一个强连通分量里的所有顶点。
时间复杂度分析:只进行了两次DFS,因而总的时间复杂度时O(V+E)。
附上代码:
#include<iostream>
#include<cstdio>
#include<vector>
#include<cstring>
using namespace std;
const int max_v=100;
int V;
vector<int>g[max_v];
vector<int>rg[max_v];
vector<int>vs;
bool used[max_v];
int cmp[max_v];
void add_edge(int from,int to)
{
g[from].push_back(to);
rg[to].push_back(from);
}
void dfs(int v)
{
used[v]=true;
for(int i=0;i<g[v].size();i++){
if(!used[g[v][i]]){
dfs(g[v][i]);
}
}
vs.push_back(v);
}
void rdfs(int v,int k)
{
used[v]=true;
cmp[v]=k;
for(int i=0;i<rg[v].size();i++){
if(!used[rg[v][i]]){
rdfs(rg[v][i],k);
}
}
}
int scc()
{
memset(used,0,sizeof(used));
vs.clear();
for(int v=0;v<V;v++){
if(!used[v]){
dfs(v);
}
}
memset(used,0,sizeof(used));
int k=0;
for(int i=vs.size()-1;i>=0;i--){
if(!used[vs[i]]){
rdfs(vs[i],k++);
}
}
return k;
}
int main()
{
scanf("%d",&V);
int m;
scanf("%d",&m);
int u,v;
for(int i=0;i<m;i++){
scanf("%d%d",&u,&v);
add_edge(u,v);
}
int ans=scc();
printf("%d\n",ans);
return 0;
}
/*
12
16
12 11
11 8
11 10
8 10
10 9
9 8
9 7
7 6
5 7
6 5
6 4
6 3
4 3
2 3
3 2
4 1
*/