上篇博客我们介绍了AOV网的拓扑序列,请参考《数据结构(七) AOV网的拓扑排序(Swift面向对象版)》。拓扑序列中包括项目的每个结点,沿着拓扑序列将项目进行下去是肯定可以将项目完成的,但是工期不是最优的。因为拓扑序列是一个串行序列,如果按照该序列执行项目,那么就是串行执行的。我们知道在一个项目中的一些子工程是可以并行来完成的,这也就类似我们的多线程。今天我们要解决的问题就是找出一个关键路径,是工期最优并保证工程的完成。什么是关键路径,我们在下方会进行详细介绍。
一、关键路径概述
在聊关键路径之前,我们先看一个简单的实例,如下图所示。我们将下方这个有向无环图看做是整个工程,将每个节点看做是该项目工程的一个子工程。子工程间又有一定的优先级。在下方图中,A的优先级最高。A做完后,B、C才可以进行开发。B、C完成后,我们才可以开发D。从下图中我们不难看出,该图的拓扑序列为A, B, C, D。如果我们按照串行的方式来完成此工程的话,那么工程完成的顺序可以是A-5->B, A-8->C, B-3->D, C-10->D。总时间为26。
从上面这个序列中我们显然可以看出来这不是最优的,因为A->B, A->C可以同时进行,B->D和C->D也可以同时进行。在允许某些子工程同时进行的情况下,A->B和A-C可以同时进行,因为A->B所需时间小于A->C所需时间,所以我们选择A->C。在A->C执行这8个小时的时间里,A->B和B->D已经执行完了,就剩下C->D了,所以关键工期为A->C->D=18。
在求关键路径的算法中,我们先求出每个事件的最早完成时间。在事件最早完成的时间集合中,工程最后一步完成的时间就是我们工程完成的最优时间。然后在工程时间最优的情况下求出每个事件最晚完成时间。如果某个时间最早的完成时间与最晚的完成时间相同,那么该事件就是我们的关键事件,该事件就位于我们关键路径中。如果这样叙述有些抽象,那么我们就拿下方这个简单图来做个类比。
在上方这个有向无环图中,我们可以求出每个事件最早发生的时间。下方截图就是每个事件所对应的最早完成的事件,因为D是工程的尾结点,所以该工程完成的最早时间也就是D完成的最早时间,即工程完成的最早时间为18。
在整个工程最早完成的时间下,我们可以从后往前推出每个子工程最晚的完成时间。这最晚完成时间就是在不耽误整个工程最小工期的前提下,最晚的完成时间。每个工程的最晚完成时间我们可以倒着推出。也就是从D=18往后推出。下方就是每个工程在保证整个工期是的完成时间是18的前提下的最晚完成时间。
对比上了最早完成时间和最晚完成时间,我们可以看出A, C, D这三个结点的最早完成时间与最晚完成时间相同,所以是我们的关键结点。这几个结点连接的路径就是我们的关键路径。所以上图中的关键路径就是A->C->D。
二、关键路径算法的具体步骤
第一部分因为示例比较简单,算是我们本篇博客的开胃小菜,接下来进入我们本篇博客真正的主题。在本部分,我们还是以原理图为主,本部分不会给出具体的代码实现,我们只讲原理。本篇博客是在上篇博客的基础上实现的,因为每个路径的最早执行的时间的计算依赖于拓扑序列,所以我们依然会采用上篇博客我们拓扑序列所使用的图的结构。下方就是我们要求关键路径的有向无环图。如果你看过前几天博客的话,那么对下方这个图的结构应该是非常熟悉了吧,今天我们依然会使用下方这个图来做我们的实例。
1.最早完成时间计算
首先我们根据拓扑排序的过程来计算出每个结点最早完成时间。最早完成时间计算的计算过程就是在拓扑排序的过程中添加一段记录每个结点完成的最早时间,下方就是求最早完成时间的整个实例图,下方会给出每一步的详细介绍。下方是由拓扑排序计算最早完成时间的具体步骤,并且给出了每一步的计算规则。
其实下方这个步骤与上一篇博客中拓扑排序的步骤大同小异,只是在其基础上引入了一个数组。数组中记录的就是索引对应结点的最早完成时间,具体步骤如下所示。下方的每一步其实就是拓扑排序的步骤,只是加入了每个结点最早完成时间的计算,因为上篇博客对拓扑排序做了详细的叙述,在此就不做过多赘述了。
-
(1)、首先我们先创建好一个数组用于存储每个结点最早完成的时间,数组元素的初始值为零,因为其实结点的完成时间就是0。
-
(2)、这一步中,结点A加入了拓扑序列,所以我们可以计算出与A结点相连结点的完成时间。因为A-10->B, A的完成时间为0,0+10,所以B的此刻的完成时间为10,同理我们可以求出F的此刻完成时间是11。
-
(3)、在第三步中,F进入拓扑序列,与F结点相连的结点是G和E。所以我们可以由F的此刻的完成时间11和F->G所需完成的时间17,求出G的此刻的完成时间11+17=28。同理我们可以求出结点E的此刻的完成时间为11+26 = 37。
-
(4)、本步骤中E结点进入拓扑序列。由上面一步我们知道E的完成时间是37,那么不难得出与E相连的结点D目前的完成时间是37+20 = 57。同理,H结点目前的完成时间为37+7=44。
-
(5)、该步骤中B结点进入拓扑序列,与B相连的结点有B-16->G, B-12->I, B-18->C。我们先来从B到G这条路径中G的完成时间,由B->G的完成时间为10+16=26。我们之前求的F->G这条路径中G的完成时间是28,因为B和F都是G的前提,B和F有一个完不成,G就无法提前完成,所以我们选择F->G这条路径来作为G的完成时间,因为FG(28)>BG(26), 低于28这个时间,G就完不成,所以此轮不更新G的完成时间。同理,此轮我们求出I的完成时间为10+12=22,C的完成时间为10+18=28。
-
(6)、本步骤中C进入拓扑序列,由C-8->I可求出C路径中I的完成时间是28+8=36,与之前求得B->I路径中I的完成时间22相比要大,所以更新I的完成时间为36。由C-22->D可求出该路径中D的完成时间为28+22=50,而上面我们计算的D的完成时间为57,50<57, 此轮不更新D的完成时间。
-
(7)、I进入拓扑序列,由I-21->D,可知,D在该路径中的完成时间为36+21=57。与之前D的完成时间相等,此轮也不更新D的完成时间。
-
(8)、G进入拓扑序列。有G-19->H可以求出,G路径上的H的完成时间为28+19=47。我们之前计算的H的完成时间为44,所以本轮将H的完成时间更新为47。
-
(9)、H进入拓扑序列。有H-16->D可以求出本轮D的完成时间为47+16=63。63大于上面计算的D的完成时间57,所以将D的完成时间更新为63。
-
(10)、D进入拓扑序列,因为D为终点,所以拓扑排序结束,我们最早完成时间也计算完毕。
经过上面这些步骤,上面数组中所存储的就是每个结点的最早完成时间,如下所示:
2.计算最迟完成时间
上面由拓扑排序从前往后计算的完成时间就是我们每个结点的最早完成时间,接下来我们将要计算每个结点在总时间不变的情况下,最晚完成时间。每个结点的最晚完成时间我们要从后往前计算,因为整工程的总时间确定,从后往前我们就可以计算每个结点最晚完成时间。下方就是计算最晚完成时间的所有的详细步骤。
因为我们是按照拓扑排序的序列从后往前计算的最晚完成时间,所以我们将拓扑序列从头到尾依次进入栈。然后以出栈的顺序来计算最晚完成时间,此刻的出栈顺序就是拓扑排序的逆序。所以下方计算每个结点的最晚完成时间时要借助栈的数据结构来完成。和上述计算最早完成时间类似,依然是将完成时间存入数组中,然后根据我们计算的数据进行更新完成时间。
下方是对最晚完成时间示例图的详细介绍:
-
(1):首先初始化我们存储最晚完成时间的数组,因为整个工程的完成时间时63,所以我们初始化每个结点的最晚完成时间就以63为准。因为再晚也不会超过63。在该步骤中,我们将D出栈。因为D是最后一个完成的结点,所以其最晚完成时间就是63,我们不做任何的更新。
-
(2):接着我们将H出栈,有H-16->D可知,H此刻的完成时间为63-16= 47,更新H对应的完成时间。
-
(3):将G出栈,由G-19->H可以计算出GH这条路径中G的完成时间为47-19=28,由G-24->D这条路径可以计算出GD这条路径中G的完成时间为63-24=39。因为28<39, 为不耽误H的正常进行,所以G此刻的最晚完成时间为28。
-
(4):将I出栈,由I-21->D这条路径,我们可以计算出,此刻I的最晚完成时间为63-21=42。
-
(5):将C出栈,由C-8->I可以计算出C在CI这条路径中最晚完成时间为42-8=34,由C-22->D这条路径可以计算出CD这条路径中C的最晚完成时间为63-22=41。因为34<41, 为了不耽误I的完成,所以C的最晚完成时间为34。
-
(6):将B出栈:有B-16->G可以计算出,BG这条路径的最晚完成时间为28-16=12,同理可计算出BI这条路径中B的最晚完成时间为42-12=30,BC这条路径中B的最晚完成时间为34-18=12。所以B的最晚完成时间为12。
-
(7):将E出栈,由E-20->D中可以求出ED这条路径中E的最晚完成时间为63-20=43,同理可求出EH这条路径中E的最晚完成时间为47-7=40,最有E的最晚完成时间为40.
-
(8):将F出栈,由F-26->E可求出FE这条路径中F的最晚完成时间为40-26=14,有F-17->G这条路径中可以求出FG这条路径中F的最晚完成时间为28-17=11,所以F的最晚完成时间为11。
-
(9):将A出栈,由A-10->B可以求出,AB这条路径中A的最晚完成时间为12-10=2,同理AF这条路径中A的最晚完成时间为11-11=0,所以A的最晚完成时间为0。至此栈中的元素为空,我们的最晚完成时间就计算完毕了,示例图如下所示。
经过上述步骤我们就可以计算出每个结点的最晚完成时间,如下所示:
3.计算关键路径
由每个结点的最早完成时间和最晚完成时间我们就可以计算出我们的关键路径了。因为工程的总时间是固定的,那些最早完成时间等于最晚完成时间的结点就是我们所要找的关键结点。下方就是在图遍历时,根据最早完成时间和最晚完成时间的对比,求出关键路径具体步骤。
-
(1):从最早和最晚完成时间中我们可以看出来关键结点有A, D, F, G, H。我们可以在遍历图时给出这几个结点的先后顺序。
-
(2):从A结点开始遍历,A与F,B相连,F的最晚时间可最早完成时间相等,所以发展成关键路径,A-11->F。
-
(3):F与E和G相连,G的最晚和最早完成时间等,所以此刻的关键路径为A-11->F-17->G。
-
(4):G与D和H相连,G的最晚时间是47-19=28得到的,所以此刻的关键路径为A-11->F-17->G-17->H。
-
(5):以此类推,可以计算出关键路径为A-11->F-17->G-17->H-16->D。
三、关键路径的代码实现
上面给出了关键路径的详细求解步骤,如果你将上面每个步骤搞明白后,给出代码实现并不难。接下来我们就会根据上面的步骤给出具体的代码实现。当然我们依然使用Swift语言实现,当然使用的是当前Swift最新版本,也就Swift3.0。
从上面的步骤中我们可以大体分为三步:
-
第一步:根据拓扑序列求出每个结点最早完成时间。
-
第二步:根据拓扑的逆序列,结合着最早完成时间求出每个结点的最晚完成时间。
-
第三步:结合着最早完成时间和最晚完成时间,根据图的结构求出关键路径。
接下来我们的代码实现也是根据上面这三步来实现的。进入我们代码实现的部分。
1.计算最早完成时间
本部分代码与上篇博客中拓扑排序的代码差不多,就多了下方红框中的部分。下方多出的代码就是在拓扑排序的过程中求出每个结点的最早完成时间,然后存储在earliestTimeOfVertex数组中。因为代码与拓扑排序的代码类似,所以在此就不做过多赘述了。
2.计算最晚完成时间
计算为最早完成时间后,我们工程的整个工期也就是定了。根据这个固定的工期,然后结合着拓扑排序的倒序,就可求出每个结点最晚完成的时间。下方这段代码就是计算每个结点的最晚完成时间。就是从后往前计算。
首先将拓扑序列入栈,也就是将拓扑序列逆序的一个过程。然后不断从栈中取值,取一个结点就要计算该结点的最晚完成时间。与该结点相连结点的最晚时间 - 权值= 该结点的最晚完成时间。在这个过程中取最小的哪个时间,就是当前结点最晚完成的时间。具体代码如下所示:
3.计算关键路径
上面两步计算完最早完成时间和最晚完成时间后,接下来我们就要开始计算我们的关键路径了。下方代码其实就是在图的层次遍历时,查找那些最早完成时间与最晚完成时间相等的结点,如果相等,则是关键路径上的结点,然后将该节点进行输出。
当然下方代码中if后方的等式是个关键,将该等式翻译成文字就是:结点最早完成时间 == 下一个结点的最晚完成时间 - 该节点到下一个结点的权值 == 该结点最晚完成时间,如果上面这个等式成立,那么就说明该结点是关键结点,我们将其进行输出。具体代码如下所示。
4.测试用例
上面三步是关键路径计算的所有代码,接下来又到了我们测试的时刻了。下方就是我们的测试用例,首先我们根据图的结点和关系创建有向图。然后输出我们创建的这个有向无环图。为了清晰的能看出每一步的执行,我们并没有将三步封装成一个函数来调用。下方的第一步就是求最早完成时间,第二步就是计算最晚完成时间,第三步就是计算我们的关键路径了。
下方就是我们的测试用例的输出结果了,输出结果还是比较直观的,有图有真相,在此就不做过多赘述了。
好今天的博客就到这儿,下几篇博客依然是关于数据结构的,敬请期待。今天博客中的Demo依然会在github上进行分享。下方是分享地址。
github分享地址:https://github.com/lizelu/DataStruct-Swift/tree/master/CriticalPath