部分转载自:http://www.tuicool.com/articles/uQBz2y
1.相关概念
给定一个有向图 G = (V, E),对于任意一对顶点 u 和 v,有 u –> v 和 v –> u,亦即,顶点 u 和 v 是互相可达的,则说明该图 G 是强连通的(Strongly Connected)。如下图中,任意两个顶点都是互相可达的。
对于无向图,判断图是否是强连通的,可以直接使用深度优先搜索(DFS)或广度优先搜索(BFS),从任意一个顶点出发,如果遍历的结果包含所有的顶点,则说明图是强连通的。
而对于有向图,则不能使用 DFS 或 BFS 进行直接遍历来判断。如下图中,如果从顶点 0 开始遍历则可判断是强连通的,而如果从其它顶点开始遍历则无法抵达所有节点。
那么,该如何判断有向图的强连通性呢?
实际上,解决该问题的较好的方式就是使用 强连通分支算法(SCC:Strongly Connected Components) ,可以在 O(V+E) 时间内找到所有的 SCC。如果 SCC 的数量是 1,则说明整个图是强连通的。
有向图 G = (V, E) 的一个强连通分支是一个最大的顶点集合 C,C 是 V 的子集,对于 C 中的每一对顶点 u 和 v,有 u –> v 和 v –> u,亦即,顶点 u 和 v 是互相可达的。
实现 SCC 的一种算法就是 Kosaraju 算法 。Kosaraju 算法基于深度优先搜索(DFS),并对图进行两次 DFS 遍历,算法步骤如下:
- 初始化设置所有的顶点为未访问的;
- 从任意顶点 v 开始进行 DFS 遍历,如果遍历结果没有访问到所有顶点,则说明图不是强连通的;
- 置换整个图(Reverse Graph);
- 设置置换后的图中的所有顶点为未访问过的;
- 从与步骤 2 中相同的顶点 v 开始做 DFS 遍历,如果遍历没有访问到所有顶点,则说明图不是强连通的,否则说明图是强连通的。
Kosaraju 算法的思想就是,如果从顶点 v 可以抵达所有顶点,并且所有顶点都可以抵达 v,则说明图是强连通的。
2. java源代码
/**找到有向图的强连通分支,正向遍历,按照后根序压栈,根据反向图查看结点是否可达*/
import java.util.*;
public class Kosaraju {
static class Graph
{
int n;
List<Integer>[] adj;
Graph(int n)
{
this.n=n;
this.adj=new List[n];
for(int i=0;i<n;i++)
{
this.adj[i]=new ArrayList<Integer>();
}
}
public void addEdge(int v,int w)
{
this.adj[v].add(w);
}
/**正向遍历,以后根序压栈,保证根先出栈*/
public void fillorder(int v,boolean[] visited,Stack<Integer> s)
{
visited[v]=true;
for(Integer i:this.adj[v])
{
if(!visited[i])
{
fillorder(i,visited,s);
}
}
s.push(v);
}
/**得到反向图*/
public Graph getTranspose()
{
Graph gv=new Graph(this.n);
for(int i=0;i<n;i++)
{
for(Integer j:this.adj[i])
{
gv.adj[j].add(i);
}
}
return gv;
}
/**DFS打印连通分支*/
public void DFSUtil(int v,boolean[] visited)
{
visited[v]=true;
System.out.print(v+" ");
for(Integer i:adj[v])
{
if(!visited[i])
{
DFSUtil(i,visited);
}
}
}
/**按照Kosaraju算法的步骤执行*/
public void printSCCs()
{
Stack<Integer> s=new Stack<Integer>();
boolean[] visited=new boolean[this.n];
for(int i=0;i<n;i++)
{
visited[i]=false;
}
/**后根序压栈*/
for(int i=0;i<n;i++)
{
if(!visited[i])
{
fillorder(i,visited,s);
}
}
/**得到反向图*/
Graph gr=this.getTranspose();
for(int i=0;i<n;i++)
{
visited[i]=false;
}
/**依据反向图算可达性*/
while(!s.empty())
{
int v=s.pop();
if(visited[v]==false)
{
gr.DFSUtil(v, visited);
System.out.println();
}
}
}
}
public static void main(String args[])
{
Graph g=new Graph(5);
g.addEdge(1, 0);
g.addEdge(0, 2);
g.addEdge(2, 1);
g.addEdge(3, 0);
g.addEdge(3, 4);
g.printSCCs();
}
}
3. 正确性证明
1. 第一次DFS有向图G时,最后记录下的节点必为最后一棵生成树的根节点。
证明:假设最后记录下节点不是树根,则必存在一节点为树根,且树根节点必为此节点祖先;而由后根序访问可知祖先节点比此节点更晚访问,矛盾;原命题成立
2. 第一次DFS的生成森林中,取两节点A、B,满足:B比A更晚记录下,且B不是A的祖先(即在第一次DFS中,A、B处于不同的生成树中);则在第二次DFS的生成森林中,B不是A的祖先,且A也不是B的祖先(即在第二次DFS中,A、B处于不同的生成树中)。
证明:假设在第二次DFS的生成森林中,B是A的祖先,则反图GT中存在B到A路径,即第一次DFS生成森林中,A是B的祖先,则A必比B更晚记录下,矛盾;假设在第二次DFS的生成森林中,A是B的祖先,则反图GT中存在A到B路径,即第一次DFS生成森林中,B是A的祖先,矛盾;原命题成立
3. 按上述步骤求出的必为强连通分量
证明:首先,证明2保证了第二次DFS中的每一棵树都是第一次DFS中的某棵树或某棵树的子树。其次,对于第二次DFS中的每棵树,第一次DFS保证了从根到其子孙的连通性,第二次DFS保证了根到子孙的反向连通性(即子孙到根的连通性);由此,此树中的每个节点都通过其根相互连通。
4. 算法复杂度
DFS遍历的时间复杂度为O(V+E) ,图的转置也为O(V+E) 。因此总的时间复杂度为O(V+E)。