作者:sunhaiyu
关键路径与无环加权有向图的最长路径
现在考虑一个这样的问题:你今天事情比较多,要洗衣服、做作业还要烧水洗澡,之后出去找朋友玩。假设洗衣服要20分钟,烧水要30分钟,做作业的话你把朋友做好的带回来抄,只需要10分钟。你想能早些去找朋友,但在那之前又必须将那些事做完,你要怎么安排呢?很容易想到,这三者同时进行:打好水开始烧水,衣服扔进洗衣机,回书桌抄作业…20分钟后作业写完了,衣服也洗好了,水还有10分钟水才烧开,利用这时间把洗好的衣服晾晒好,差不多水也烧开了,好了最后去洗澡。简直一气呵成,这是我们能花费的最少时间了,在这个例子中刚好等于所有任务中持续时间最长的那个。(你做完了作业才想起来去烧水,花费不止半小时吧)
由此引申出一个更为广泛的问题,给定一组需要完成的任务和每个任务所需的时间,以及一组关于任务完成的先后次序的优先级限制。在满足限制条件的前提下应该如何在若干相同的处理器上(数量不限,可并行处理多个任务)安排任务并在最短的时间内完成所有的任务?
此问题的提出主要是为了解决并行任务调度,使得完成所有任务的总时间最短。待处理的任务总数可能成百上千,因此需要一个算法帮我们快速规划一个调度方案:按照怎样的顺序执行这些任务,哪些任务可以同时处理,如何使得耗费的总时间最短?正好存在一种叫做“关键路径”的方法可以证明这个问题与无环加权有向图的最长路径问题等价。
关键路径:把路径上各个任务所持续的时间之和称为路径长度,从起点到终点的所有路径中,具有最长路径长度的路径称为关键路径,关键路径中的各个任务称为关键任务。上面的例子中,烧水就是个关键任务。
首先,按照关键路径的顺序执行任务,一定能保证所有的任务都能完成,且此时花费的总时间最短。有些活动顺序进行,有些活动并行进行。从起点到各个顶点,以至从起点到终点的有向路径可能不止一条,这些路径的长度也不尽相同。这若干条从起点到终点的路径可以看做一个生产过程的几条不同的生产线,必须每条生产线都完工,整个生产过程才算结束,也就是不论如何你都得等那条花费时间最长的流水线做完,整个生产才可能完工。现在由于可以同时处理多个任务,在花费时间最长的流水线工作过程中,其他流水线一定会提前完工,因此花费时间最长的流水线做完后,整个工程也随之竣工了。假设花费时间最长的那条流水线所用的时间是M,这就是说,不管怎么安排,都需要至少M的时间才能竣工,而这已经是最短时间了。
再举个例子,你和朋友们约好去某个地方聚餐。有些朋友到的比较早,有些朋友到得比较晚,但是不管怎么样,我们都要等到最后一个朋友到目的地,这样大家才算是聚齐了。
说了半天,求并行任务调度中的关键路径,实际上就是求从起点到终点的最长路径。
通过求解最长路径得到关键路径
通过上面的讨论,现在只需求最长路径,就能得到关键路径。我们知道任务调度必须要求图是无环的,因此可以使用求无环加权有向图的最短路径的方法求最长路径。
具体方法是:复制原图得到一个副本,将副本的所有边的权重取相反数,求副本的最短路径实际上就是原图的最长路径。
或者一个更为简单的方法:修改边的放松方法。改为distTo[v] + edge.weight() > distTo[w](求最短路径的不等号是<),即:有比原来到w更长的路径就更新。同时初始化的时候,distTo[i]从原来的正无穷改成负无穷。
求无环加权有向图的最短路径,可以按照拓补排序依次放松顶点。详细的见我上一遍文章,只需改前述两个地方,就能求得最长路径。
package Chap7; import java.util.LinkedList; /** * 求无环有向图的最长路径 */ public class AcycliLP { private DiEdge[] edgeTo; private double[] distTo; public AcycliLP(EdgeWeightedDiGraph<?> graph, int s) { edgeTo = new DiEdge[graph.vertexNum()]; distTo = new double[graph.vertexNum()]; for (int i = 0; i < graph.vertexNum(); i++) { // 1. 改成了负无穷 distTo[i] = Double.NEGATIVE_INFINITY; } distTo[s] = 0.0; // 以上是初始化 TopoSort topo = new TopoSort(graph); if (!topo.isDAG()) { throw new RuntimeException("该图存在有向环,本算法无法处理!"); } for (int v : topo.order()) { relax(graph, v); } } private void relax(EdgeWeightedDiGraph<?> graph, int v) { for (DiEdge edge : graph.adj(v)) { int w = edge.to(); // 2、若路径更长就更新 if (distTo[v] + edge.weight() > distTo[w]) { distTo[w] = distTo[v] + edge.weight(); edgeTo[w] = edge; } } } public double distTo(int v) { return distTo[v]; } public boolean hasPathTo(int v) { return distTo[v] != Double.POSITIVE_INFINITY; } public Iterable<DiEdge> pathTo(int v) { if (hasPathTo(v)) { LinkedList<DiEdge> path = new LinkedList<>(); for (DiEdge edge = edgeTo[v]; edge != null; edge = edgeTo[edge.from()]) { path.push(edge); } return path; } return null; } }
好,可以求得关键路径了。现在来看一个任务调度的例子,如何利用上面的实现来安排任务。