上篇博客我們介紹了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