介绍拓扑排序之前首先介绍一下调度问题
一种应用广泛的模型是给定一组任务并安排它们的执行顺序,限制条件是这些任务的执行方法和起始时间。最重要的一种限制条件叫做优先级限制,它指明了哪些任务必须在哪些任务之前完成。
优先级限制下的调度问题:
给定一组需要完成的任务,以及一组关于任务完成的先后次序的优先级限制。在满足限制条件的前提下应该如何安排并完成所有任务?
在有向图中,优先级限制下的调度问题等价于下面的这个基本的问题。
拓扑排序的定义是:给定一幅有向图,将所有的顶点排序,使得所有的有向边均从排在前面的元素指向排在后面的元素(或者说明无法做到这一点)。
拓扑排序
拓扑排序的典型应用
任务调度,课程安排,即成,电子表格,符号链接
一般来说,如果一个有优先级限制的问题中存在有向环,那么这个问题肯定是无解的。要检查这种错误,需要进行有向环检测。
一幅有向图中含有的环的数量可能是图的大小的指数级别,因此我们只需要找出一个环即可,而不是所有环。一旦我们找到了一条有向边v-w且w已经存在于栈中,就找到了一个环。因为栈表示的是一条由w到v的有向路径,而v-w正好补全了这个环。
寻找有向环的代码如下:
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<>();
for(int x=v;x!=w;x=edgeTo[x])
cycle.push(x);
cycle.push(w);
cycle.push(v);
}
onStack[v]=false;
}
public boolean hasCycle(){
return cycle!=null;
}
public Iterable<Integer> cycle(){
return cycle;
}
}
该类为标准递归dfs()方法添加了一个布尔类型的数组onStack[]来保存递归调用期间栈上的所有顶点。
在执行dfs(G,v)时,查找的是一条由起点到v的有向路径。要保存这条路径,DirectedCycle维护了一个由顶点索引的数组onStack[],以标记递归调用的栈上的所有顶点(在调用dfs(G,v)时将onStack[v]设为true,在调用结束时将其设为false)。
优先级限制下的调度问题等价于计算有向无环图中的所有顶点的拓扑顺序。当且仅当一幅有向图时无环图时它才能进行拓扑排序。
有向图中基于深度优先搜索的顶点排序的基本思想是深度优先搜索正好只会访问每个顶点一次。如果将dfs()的参数顶点保存在一个数据结构中,遍历这个数据结构实际上就能访问途中的所有顶点,遍历的顺序取决于这个数据结构的性质以及是在递归调用之前还是之后进行保存。有以下3种顺序:
前序:在递归调用之前将顶点加入队列。
后序:在递归调用之后将顶点加入队列。
逆后序:在递归调用之后将顶点压入栈。
下面算法是有向图种基于深度优先搜索的顶点排序
public class DepthFirstOrder {
private boolean[] marked;
private Queue<Integer> pre; //所有顶点的前序排列
private Queue<Integer> post; //所有顶点的后序排列
private Stack<Integer> reversePost; //所有顶点的逆后序排列
public DepthFirstOrder(Diagraph G) {
pre=new Queue<>();
post=new Queue<>();
reversePost=new Stack<>();
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;
}
}
下面是拓扑排序算法
public class Topological {
private Iterable<Integer> order; //顶点的拓扑顺序
public Topological(Diagraph G) {
DirectedCycle cyclefinder=new DirectedCycle(G);
if(!cyclefinder.hasCycle()){
DepthFirstOrder dfs=new DepthFirstOrder(G);
order=dfs.reversePost();
}
}
public Iterable<Integer> order(){
return order;
}
public boolean isDAG(){
return order!=null;
}
}
一幅有向无环图的拓扑顺序即为所有顶点的逆后序排列。
证明:对于任意边v-w,在调用dfs(v)时,下面三种情况必有其一成立。
dfs(w)以及被调用过且已经返回了(w已经被标记)。
dfs(w)还没有被调用(w还未被标记),因此v-w会直接或间接调用并返回dfs(w),且dfs(w)会在dfs(v)返回前返回。
dfs(w)已经被调用但还未返回。
Topological类的实现使用了深度优先搜索来对有向无环图进行拓扑排序。
解决任务调度类应用通常需要3步:
1.指明任务和优先级条件
2.不断检测并去除有向图中的所有环,以确保存在可行方案
3.使用拓扑排序解决调度问题