1.1 术语
有向图是由一组顶点和一组有方向的边组成的,每条有方向的边都连接着有序的一对顶点。
有向图中,一个顶点的出度为由该顶点指出的边的总数;一个顶点的入度为指向该顶点的边的总数。
有向图中,有向路径由一系列顶点组成,对于其中的每个顶点都存在一条有向边从它指向序列中的下一个顶点。有向环为一条至少含有一条边且起点和终点相同的有向路径。简单有向环是一条(除了起点和终点)不含重复顶点和边的环。路径或者环的长度即为其中包含的边数。
1.2 有向图的可达性
在有向图中,深度优先搜索标记由一个集合的顶点可达的所有顶点所需的时间与被标记的所有顶点的出度之和成正比。
多点可达性的一个重要实际应用是在典型的内存管理系统中,包括许多Java的实现。在有向图中,一个点表示一个对象,一条边则表示一个对象对另一个对象的引用。这个模型很好地表现了Java程序的内存使用状况。在程序执行的任何时候都有某些对象是可以被直接访问的,而不能通过这些对象访问到的所有对象都应该被回收以便释放内存。标记-清楚的垃圾回收策略会为每个对象保留一个位做垃圾收集之用。它会周期性地运行一个有向图可达性算法来标记所有可以被访问到的对象,然后清理所有对象,回收没有被标记的对象,以腾出内存为新的对象使用。
1.3 环和有向无环图
1.3.1 有向图中的环
寻找有向环的算法如下:
public class DirectedCycle {
private boolean[] marked;
private int[] edgeTo;
private Stack<Integer> cycle; // 有向环中的所有顶点
private boolean[] onStack; // 递归调用栈上的所有顶点
public DirectedCycle (Diagraph G) {
onStack = new boolean[G.V()];
edgeTo = new int[G.V()];
marked = new boolean[G.V()];
for (int v = 0; v < G.V() ; v++ ) {
if (!marked[v]) {
dfs(G, v);
}
}
}
private void dfs(Diagraph G, int v) {
onStack[v] = true;
marked[v] = true;
for (int w : G.adj(v) ) {
if (this.hasCycle()) {
return;
}else if (!marked[w]) {
edgeTo[w] = v;
dfs(G, w);
}else if (onStack[w]) {
cycle = new Stack<Integer>();
for (int x = v; x != w ; x = edgeTo[x] ) {
cycle.push(x);
}
cycle.push(w);
cycle.psuh(v);
}
}
onStack[v] = false;
}
public boolean hasCycle() {
return cycle != null;
}
public Iterable<Integer> cycle(){
return cycle;
}
}
该类为标准的递归dfs方法添加了一个布尔类型的数组onStack[]来保存递归调用期间栈上的所有顶点。当它找到一条边v-w并且w在栈中时,它就找到了一个有向环。环上所有顶点可通过edgeTo得到。
1.3.2 顶点的深度优先次序与拓扑排序
优先级限制下的调度问题等价于计算有向无环图中的所有顶点的拓扑排序。
当且仅当一个有向图它是无环图时它才能进行拓扑排序。
有向图中基于深度优先搜索的顶点排序:
public class DepthFirstOrder {
private boolean[] marked;
private Queue<Integer> pre; // 前序排列(在递归调用之前将顶点加入队列)
private Queue<Integer> post; // 后序排列
private Queue<Integer> reversePost; // 逆后续排列(在递归调用之后把顶点压入栈)
public DepthFirstOrder (Diagraph G) {
pre = new Queue<Integer>();
post = new Queue<Integer>();
reversePost = new Queue<Integer>();
marked = new boolean[G.V()];
for (int v = 0; v < G.V() ; v++ ) {
if (!marked[v]) {
dfs(G, v);
}
}
}
private void dfs(Diagraph G, int v) {
pre.enqueue(v);
marked[v] = true;
for (int w : G.adj(v) ) {
if (!marked[w]) {
dfs(G,w);
}
}
post.enqueue(v);
reversePost.push(v);
}
public Iterable<Integer> pre(){
return pre;
}
public Iterable<Integer> post(){
return post;
}
public Iterable<Integer> reversePost(){
return reversePost;
}
}
一个有向无环图的拓扑排序即为所有顶点的逆后续排列。
使用深度优先搜索对有向无环图进行拓扑排序所需的时间和V+E成正比。
1.4 有向图的强连通性
如果两个顶点v和w是互相可达的,则称它们为强连通的。也就是说,既存在一条从v到w的有向路径,也存在一条从w到v的有向路径。如果一个有向图的任意两顶点都是强连通的,则称这个有向图也是强连通的。
1.4.1 强连通分量
作为一种平等关系,强连通性将所有顶点分为了一些平等的部分,每个部分都是由相互均为强连通的顶点的最大子集组成的。这些子集称为强连通分量。
1.4.2 Kosaraju算法
计算强连通分量的Kosaraju算法如下:
public class KosarajuSCC {
private boolean[] marked;
private int[] id; // 强连通分量的标识符
private int count; // 强连通分量的数量
public KosarajuSCC (Diagraph G) {
marked = new boolean[G.V()];
id = new int[G.V()];
DepthFirstOrder order = new DepthFirstOrder(G.reverse());
for (int s : order.reversePost() ) {
if (!marked[s]) {
dfs(G, v);
count++;
}
}
}
private void dfs(Diagraph G, int v) {
marked[v] = true;
id[v] = count;
for (int w : G.adj(v) ) {
if (!marked[w]) {
dfs(G,w);
}
}
}
public boolean stronglyConnected(int v, int w){
return id[v] = id[w];
}
public int id(int v){
return id[v];
}
public int count(){
return count;
}
}
使用深度优先搜索查找给定有向图G的反向图GR,根据由此得到的所有顶点的逆后序再次用深度优先搜索处理有向图G(Kosaraju算法),其构造函数中的每一次递归调用所标记的顶点都在同一个强连通分量中。
Kosaraju算法的预处理所需的时间和空间与V+E成正比且支持常数时间的有向图强连通性的查询。