优先级限制下的调度问题
给定一组需要完成的任务,以及一组关于任务完成的先后次序的优先级限制。在满足限制条件的前提下应该如何安排并完成所有任务?
对于任意一个这样的问题,我们都可以马上画出一张有向图,其中顶点对应任务,有向边对应优先级顺序。
在有向图中,优先级限制下的调度问题等价于下面这个基本的问题。
拓扑排序。给定一幅有向图,将所有的顶点排序,使得所有的有向边均从排在前面的元素指向排在后面的元素。
有向图中的环
一般来说,如果一个有优先级限制的问题中存在有向环,那么这个问题肯定是无解的。要检查这种错误,需要解决下面这个问题。
有向环检测。给定有向图中包含有向环吗?如果有,按照路径的方向从某个顶点并返回自己来找到环上的所有顶点。
有向无环图:有向无环图(DAG)就是一幅不含有向环的有向图。
解决有向环检测的问题可以回答下面这个问题:一幅有向图是有向无环图吗?基于深度优先搜索来解决这个问题并不困难,因为由系统维护的递归调用的栈表示的正是“当前”正在遍历的有向路径。一旦我们找到了一条有向边v->w且w已经存在于栈中,就找到了一个环,因为栈表示的是一条由w到v的有向路径,而v->w正好补全了这个环。同时,如果没有找到这样的边,那就意味着这幅有向图是无环的。
有向环的API
API | 功能 |
---|---|
DirectedCycle(Digraph G) | 寻找有向环的构造函数 |
boolean hasCycle() | G是否含有有向环 |
Iterable<Integer> cycle() | 有向环中的所有顶点(如果存在的话) |
代码
package section4_2;
import java.util.ArrayList;
import java.util.List;
public class DirectedCycle {
private boolean[] onStack;
private boolean[] marked;
private int[] edgeTo;
private List<Integer> cycle;
public DirectedCycle(Digraph 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);
}
}
public void dfs(Digraph 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 ArrayList<>();
for (int x = v;x != w;x = edgeTo[x]) {
cycle.add(0,x);
}
cycle.add(0,w);
cycle.add(0,v);
}
}
onStack[v] = false;
}
public boolean hasCycle() {
return cycle != null;
}
public Iterable<Integer> cycle() {
return cycle;
}
public static void main(String[] args) {
int[][] data = {
{0,1},
{1,2},
{2,3},
{3,1}
};
int vn = 4;
int en = 4;
Digraph digraph = new Digraph(vn,en,data);
DirectedCycle directedCycle = new DirectedCycle(digraph);
if (directedCycle.hasCycle()) {
for (int x : directedCycle.cycle()) {
System.out.print(x + " ");
}
}
}
}
顶点的深度优先次序与拓扑排序
优先级限制下的调度问题等价于计算有向无环图中的所有顶点的拓扑顺序。
当且仅当一幅有向图是无环图时它才能进行拓扑排序。
有向图中基于深度优先搜索的顶点排序
它的基本思想是深度优先搜索正好只会访问每个顶点一次。如果将dfs()的参数顶点保存在一个数据结构中,遍历这个数据结构实际上就能访问图中的所有顶点,遍历的顺序取决于这个数据结构的性质以及是在递归调用之前还是之后进行保存。在典型的应用中,人们感兴趣的是顶点的以下3种排列顺序:
- 前序:在递归调用之前将顶点加入队列。
- 后序:在递归调用之后将顶点加入队列。
- 逆后序:在递归调用之后将顶点压入栈。
输入:
输出次序:
代码:
package section4_2;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
public class DepthFirstOrder {
private boolean[] marked;
private Queue<Integer> pre;
private Queue<Integer> post;
private List<Integer> reversePost;
public DepthFirstOrder(Digraph G) {
pre = new LinkedList<>();
post = new LinkedList<>();
reversePost = new ArrayList<>();
marked = new boolean[G.V()];
for (int v = 0;v < G.V();v++) {
if (!marked[v]) {
dfs(G,v);
}
}
}
private void dfs(Digraph G, int v) {
pre.offer(v);
marked[v] = true;
for (int w : G.adj(v)) {
if (!marked[w]) {
dfs(G,w);
}
}
post.offer(v);
reversePost.add(0,v);
}
public Iterable<Integer> pre() {
return pre;
}
public Iterable<Integer> post() {
return post;
}
public Iterable<Integer> reversePost() {
return reversePost;
}
}
拓扑排序
一幅有向无环图的拓扑顺序即为所有顶点的逆后序排列。
拓扑排序API
API | 功能 |
---|---|
Topological(Digraph G) | 拓扑排序的构造函数 |
boolean isDAG() | G是有向无环图吗 |
Iterable<Integer> order() | 拓扑有序的所有顶点 |
代码:
package section4_2;
public class Topological {
private Iterable<Integer> order;
public Topological(Digraph 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;
}
public static void main(String[] args) {
int[][] data = {
{0,5},
{0,1},
{0,6},
{2,0},
{2,3},
{3,5},
{5,4},
{6,4},
{7,6},
{8,7},
{6,9},
{9,10},
{9,11},
{9,12},
{11,12}
};
int vn = 13;
int en = 15;
Digraph digraph = new Digraph(vn,en,data);
Topological topo = new Topological(digraph);
for (int v : topo.order()) {
System.out.print(v + " ");
}
}
}
输出:
在实际应用中,拓扑排序和有向环的检测总会一起出现,因为有向环的检测是排序的前提。因此,解决任务调度类应用通常需要以下三步:
- 指明任务和优先级条件;
- 不断检测并去除有向图中的所有环,以确保存在可行的方案;
- 使用拓扑排序解决调度问题。