【数据结构与算法基础】AOE网络与关键路径
前言
數據結構,一門數據處理的藝術,精巧的結構在一個又一個算法下發揮著他們無與倫比的高效和精密之美,在為信息技術打下堅實地基的同時,也令無數開發者和探索者為之著迷。
也因如此,它作為博主大二上學期最重要的必修課出現了。由于大家對于上學期C++系列博文的支持,我打算將這門課的筆記也寫作系列博文,既用于整理、消化,也用于同各位交流、展示數據結構的美。
此系列文章,將會分成兩條主線,一條“數據結構基礎”,一條“數據結構拓展”?!皵祿Y構基礎”主要以記錄課上內容為主,“拓展”則是以課上內容為基礎的更加高深的數據結構或相關應用知識。
歡迎關注博主,一起交流、學習、進步,往期的文章將會放在文末。
話說在前面的章節,我們介紹了一種工程上常用的AOV網絡,他可以清晰的表示工程中每項活動的先后順序,并計算出滿足要求的活動規劃,也就是拓撲排序。
這一節,我們將繼續研究這類網絡。
AOE網絡
在一般工程項目中,我們不僅會關心每項活動的先后次序,更會關心活動之間更加嚴格的時間約束關系。
不妨改造AOV網絡的有向無環圖,在此基礎上引入邊權,使其成為有向有權無環圖。
以一條有向邊代替一個活動,邊權為其進行的時間長度,邊的起點表示該任務的開始,終點表示該任務的完成。從點的角度來看,出邊表示一個活動開始的狀態,也稱為一個事件,入邊表示一個任務的完成。
這樣的圖被稱作AOE網絡(ActivityOnEdgeNetworkActivity\ On\ Edge\ NetworkActivity?On?Edge?Network)。如果比較他和AOV網絡的名稱,不難發現他將關注的重點從頂點變成了邊,這也將是兩個網絡處理問題和算法不同之處的來源。
在工程中由于一般只有一個起始點和一個完成點,所以在正常的情況下,一張AOE網絡中也只有一個源點和匯點。其中源點入度為0,匯點出度為0。如下圖:
在AOE網絡中,我們關心的問題不在僅僅是任務進行的先后順序,更包括了每個任務乃至整個工程項目的計劃完成時間。一般來說,一些活動能夠同時開工,他們會從同一頂點出發。有些狀態必須等到若干前置工作的完成,這些任務會匯集到一個頂點處。
畫出工程對應的AOE網絡可以使我們得出:
- 完成整個工程至少需要多少時間
- 為縮短完成工程所需要的時間,應當加快那些活動
- 為了不延誤整個工程,哪些活動不得延期,那些活動可以適當延期
關鍵路徑
根據上文的敘述,不難發現整個工程完成的最短時間,取決于從源點到匯點的最長路徑的長度。我們將這個路徑定義為該網絡的關鍵路徑。上文圖中的關鍵路徑就如下所示:
在關鍵路徑中的任何一環不能按時完成,就會延誤整個項目的進度。如在上圖中,標紅的路徑中任意一條路徑的長度增加都會使得源點到匯點的最長路徑增加。而增加其他邊則不一定。
需要注意的是,關鍵路徑也有可能存在多條,定義中并沒有限制一張AOE網絡只能有一條關鍵路徑。如將上圖中的權值稍作修改,就會出現多條關鍵路徑:
在關鍵路徑上所有的活動都是關鍵活動。(需要注意的是在AOE網絡中,活動是邊而不是頂點)
多條關鍵路徑的來源往往就在于一些狀態之間可能存在著不同的活動路徑,他們的時間加和相同(就像上圖中的狀態15之間存在兩條路徑)。而不同的路徑在經過相同的頂點時遵循著乘法原理(上圖為1×21\times21×2)
算法設計
有了對關鍵路徑的定義和分析,下面的重點就是如何從AOE網絡中尋找關鍵路徑。
這里介紹一種基于拓撲排序的算法思路:
該算法基于拓撲排序算法,改進也并不大,只是在記錄頂點的入度和出度之余多記錄了源點到他的最長路徑。
代碼實現
路徑長度
與關鍵路徑相關的需求有很多,不妨先擬定一個需求,僅求出關鍵路徑的長度:
給定一張有向有權無環的AOE網絡,求出其源點到匯點的關鍵路徑長度
輸入格式:
第一行包含兩個整數n,m,表示頂點的個數和邊的條數
接下來m行,每行3個整數表示每條邊的起點,終點和權值
存儲格式采用鄰接表,頂點信息定義如下:
struct Edge{int vertex;Edge * next;int len;Edge(int vertex,Edge * next){this->vertex = vertex;this->next = next;this->len = len;} }; Edge *head[N];//邊鏈表頭結點數組 int id[N];//入度 int od[N];//出度 int dis[N];//頂點到源點的最長距離代碼實現如下:
void link(int x,int y,int len){Edge * edge = new Edge(y,head[x],len);head[x] = edge; } int queue[N];//隊列 int main(){int n,m;cin >> n >> m;for(int i = 0,x,y,len;i < m;i++){//讀入邊cin >> x >>y >>len;od[x]++;id[y]++;link(x,y,len);}int l = 1,r = 0;//隊列左右下標for(int i = 1;i <= n;i++){if(id[i] == 0){//入度為0的頂點入隊queue[++r] = i;}}int k;while(l <= r){//遍歷隊列,直至為空k = queue[l++];for(Edge * edge = head[k];edge != NULL;edge = edge->next){id[edge->vertex]--;//刪除邊if(id[edge->vertex] == 0){//如果后繼節點入度為0,該節點入隊queue[++r] = edge->vertex;}if(dis[edge->vertex] < dis[k] + edge->len){//使用當前頂點更新相連頂點的最長距離dis[edge->vertex] = dis[k] + edge->len;}}}cout << dis[queue[r]] << endl;//匯點一定在隊列的最后,想想這是為什么? }編譯執行程序,輸入上面的示例圖:
打印路徑
一個程序能找出關鍵路徑長度往往是不夠的,我們還需要得出關鍵路徑上的關鍵活動。
那么如果題目需求輸出一條關鍵路徑應該怎么處理呢?
這個問題其實很好解決,我們在記錄每條邊的距離源點的最遠距離的同時記錄其最遠路徑上的前驅。完成算法后從匯點開始,便可以順藤摸瓜的找到一條從源點到匯點的關鍵路徑上的全部頂點。
下面讓我們來修改上面擬定的需求:
給定一張有向有權無環的AOE網絡,求出其源點到匯點的關鍵路徑長度,并輸出任意一條關鍵路徑
輸入格式:
第一行包含兩個整數n,m,表示頂點的個數和邊的條數
接下來m行,每行3個整數表示每條邊的起點,終點和權值
圖的存儲結構與上文相同,現需要額外定義一個最長路徑的前驅頂點數組:
int pre[N];//到達該頂點的最長路徑上的前驅結點算法代碼實現如下:
void print(int k){//遞歸打印一條路徑,由于頂點編號從1開始,所以值為0表示沒有前驅if(k == 0){return;}print(pre[k]);cout << k << " "; } void link(int x,int y,int len){Edge * edge = new Edge(y,head[x],len);head[x] = edge; } int queue[N];//隊列 int main(){int n,m;cin >> n >> m;for(int i = 0,x,y,len;i < m;i++){//讀入邊cin >> x >>y >>len;od[x]++;id[y]++;link(x,y,len);}int l = 1,r = 0;//隊列的左右下標for(int i = 1;i <= n;i++){if(id[i] == 0){//入度為0的頂點入隊queue[++r] = i;}}int k;while(l <= r){//循環遍歷隊列中的頂點直至隊列為空k = queue[l++];for(Edge * edge = head[k];edge != NULL;edge = edge->next){id[edge->vertex]--;//刪除該邊if(id[edge->vertex] == 0){//如果后繼節點入度為0,后繼節點入隊queue[++r] = edge->vertex;}if(dis[edge->vertex] < dis[k] + edge->len){//使用當前結點更新后繼節點最長路徑值和前驅dis[edge->vertex] = dis[k] + edge->len;pre[edge->vertex] = k;}}}cout << dis[queue[r]] << endl;//匯點一定在隊列的末尾,想想這是為什么?print(queue[r]);//打印源點到匯點的路徑 }再執行上述代碼:
打印所有路徑
當然,能打印一條路徑可能還不夠過癮。
那么如果需要我們打印所有路徑應該怎么辦呢?
辦法總是有的,就是得多掉幾根頭發
——哲茨海氏沃茨基碩德
關鍵路徑不唯一,上面方法不能夠打印所有的原因在于一個頂點可能是多條關鍵路徑的交匯頂點。換句話說,就是一個頂點可能有多個前驅,而前驅數組只能記錄其中的一個。
其實本來源點到匯點的關鍵路徑就不像是前驅鏈表表示的那樣是一條鏈,更像是一張圖。
所以問題的關鍵就在于如何能找到一個頂點的所有的前驅。
方法其實也很簡單,對于一個頂點kkk來說,只要枚舉所有指向它的頂點iii,并從中選出所有滿足dis[k] = dis[i] + len[i][k]的頂點,他們就是kkk的前驅。
對于上圖來說,
8號頂點的前驅中只有5號滿足dis[8] = dis[5] + 8
而5號頂點的兩個前驅2和3都分別滿足dis[5] = dis[2] + 1和dis[5] = dis[3] + 3
所以只要根據這個方式,就可以找到一個頂點在關鍵路徑中的所有前驅,并依次進行遞歸。
什么?你問我怎么找一個頂點的前驅?我們的邊都是有向邊,只能向后找不能向前找?
不過也不是沒有解決辦法,我們可以將所有的邊都存儲一個反向的邊,并且在邊結點中加上一個字段標記正邊還是反邊。在拓撲排序過程中只用正邊,獲取路徑的過程中只用反邊就好啦~
為此,我們需要修改鄰接表結點的定義:
執行示例,發現兩條關鍵路徑:
往期博客
- 【數據結構基礎】數據結構基礎概念
- 【數據結構基礎】線性數據結構——線性表概念 及 數組的封裝
- 【數據結構基礎】線性數據結構——三種鏈表的總結及封裝
- 【數據結構基礎】線性數據結構——棧和隊列的總結及封裝(C和java)
- 【算法與數據結構基礎】模式匹配問題與KMP算法
- 【數據結構與算法基礎】二叉樹與其遍歷序列的互化 附代碼實現(C和java)
- 【數據結構與算法拓展】 單調隊列原理及代碼實現
- 【數據結構基礎】圖的存儲結構
- 【數據結構與算法基礎】并查集原理、封裝實現及例題解析(C和java)
- 【數據結構與算法拓展】二叉堆原理、實現與例題(C和java)
- 【數據結構與算法基礎】哈夫曼樹與哈夫曼編碼(C++)
- 【數據結構與算法基礎】最短路徑問題
- 【數據結構與算法基礎】堆排序原理及實現
- 【數據結構與算法基礎】最小生成樹算法原理及實現
- 【數據結構基礎】圖的遍歷方法與應用
- 【數據結構基礎】矩陣的存儲結構,數組,三元組表及十字鏈表
- 【數據結構與算法基礎】拓撲排序與AOV網絡
參考資料:
- 《數據結構》(劉大有,楊博等編著)
- 《算法導論》(托馬斯·科爾曼等編著)
- OI WiKi
總結
以上是生活随笔為你收集整理的【数据结构与算法基础】AOE网络与关键路径的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C#带参方法
- 下一篇: 论文翻译-ASTER: An Atten