引言
之前所說的拓撲排序是為了解決一個工程能否順利進行的問題。但在生活中,我們還會經常遇到如何解決工程完成需要的最短時間問題。
舉個例子,我們需要制作一台汽車,我們需要先造各種各樣的零件,然后進行組裝,這些零件基本上都是在流水線上同時成產的。加入造一個輪子需要0.5天的時間,造一個發動機需要3天的時間,造一個車底盤需要2天的時間,造一個外殼需要2天的時間,其他零部件需要2天的時間,全部零部件集中到一個地方需要0.5天的時間,組裝成車需要2天的時間,那么請問汽車廠造一輛車,最短需要多少時間。
因為這些零部件都是分別在流水線上同時生產的,也就是說在生產發動機的這3天當中,可能已經生產了6個輪子,1.5個外殼,1.5個車底盤,而組裝是在這些零部件都生產好之后才能進行,因此最短的時間就是零件生產時間最長的發動機3天+集中零部件0.5天+組成成車2天,以供需要5.5天完成一輛車的生產。
所以,我們如果對一個流程獲得最短時間,就需要分析它的拓撲關系,找到最關鍵的流程,這個流程的時間就是最短的時間。
AOE網的定義
在一個表示工程的帶權有向圖中,用頂點表示時間,用有向邊表示活動,用邊上的權值表示活動的持續時間,這種有向圖的邊表示活動的網,我們稱之為AOE網。
把AOE網中沒有入邊的頂點稱之為始點或源點,沒有出邊的頂點稱之為終點或匯點。
摘自:《大話數據結構》
關鍵路徑的定義
我們把路徑上各個活動所持續的時間之和稱之為路徑長度,從源點到匯點具有最大的長度的路徑叫做關鍵路徑,在關鍵路徑上的活動叫關鍵活動。
對上面的AOE網來說,其關鍵路徑就是:開始->發動機完成->部件集中->組裝完成。路徑長度為5.5。
關鍵路徑算法原理
對於一個活動來說,有活動的最早開始時間和最晚開始時間,比較這個活動的最早開始時間和最晚開始時間,如果最早開始時間和最晚開始時間相等,那么就意味着該活動是關鍵活動,活動間的路徑為關鍵路徑。如果不相等,那么這個活動就不是關鍵活動。
名詞解釋
- 事件的最早發生時間etv:即頂點 的最早發生時間
- 事件的最晚發生時間ltv:即頂點 的最晚發生時間,也就是每個頂點對應的事件最晚需要開始的時間,超過此時間將會延誤整個工期。
- 活動的最早開工時間ete:即弧 的最早發生時間
- 活動的最晚開工時間lte:即弧 的最晚發生時間,也就是不推遲工期的最晚開工時間。
算法分析
假設起點為
,則我們從
到
的最長路徑的長度為頂點(事件)
的最早發生時間。
同時
的最早發生時間也是所有以
為尾的弧所表示的活動的最早開工時間(即ete),
定義活動最晚開工時間表示在不會延誤所有工期(即lte)。通過根據ete[k]是否與lte[k]相等來判斷
是關鍵活動。
同時,因為我們采用鄰接表來存儲AOE網,並且對於表示活動的每條邊都具有活動的持續時間(即該邊的權重),所以,我們需要修改邊表節點,增加一個weight來存儲弧的權值。
首先我們要求頂點
即求etv[k]的最早發生時間,公式:
在計算ltv時,其實就是對拓撲序列倒過來進行,所以我們可以計算頂點
即求ltv[k]的最晚發生時間。公式:
這兩個公式怎么理解呢
我的理解是,對於事件
的最早發生時間,表現為以
為弧頭,
為弧尾的其他所有弧(注意:i的值可能沒有,表示
的入度為0;可能為n,表示
的入度為n)的活動持續時間+
的最早開工時間列表中的最大值。以上面的流程圖為例,零部件集中這項事件只有等生產時間最長的發動機造好之后才能進行。
對於
的最晚發生時間,表現為事件
的最晚發生時間 減去 以
為弧尾,
為弧頭的其他所有弧(注意:j的值可能沒有,表示
的出度為0;可能為n,表示
的出度為n)的活動持續時間。
求關鍵路徑的步驟
- 根據圖的描述建立該圖的鄰接表
- 從源點 出發,根據拓撲序列算法求源點到匯點每個頂點的etv,如果得到的拓撲序列個數小於網的頂點個數,則該網中存在環,無關鍵路徑,結束程序。
- 從匯點 出發,且ltv[n-1]=etv[n-1],按照逆拓撲序列,計算每個頂點的ltv。
- 根據每個頂點的etv和ltv求每條弧的ete和lte,若ete=lte,說明該活動是關鍵活動。
代碼實現
示例AOE網:
該網的鄰接表結構:
數據結構
邊表節點:
public class EdgeNode {
public int adjevex;
public int weight;
public EdgeNode next;
public EdgeNode(int adjevex, EdgeNode next) {
this.adjevex = adjevex;
this.next = next;
}
public EdgeNode(int adjevex, int weight, EdgeNode next) {
this.adjevex = adjevex;
this.weight = weight;
this.next = next;
}
}
頂點節點:
public class VertexNode {
public int in;
public Object data;
public EdgeNode firstedge;
public VertexNode(Object data, int in, EdgeNode firstedge) {
this.data = data;
this.in = in;
this.firstedge = firstedge;
}
}
通過拓撲排序求得etv
public boolean ToplogicalSort() {
EdgeNode e;
int k, gettop;
int count = 0;
etv = new int[adjList.length];
for (int i = 0; i < adjList.length; i++) {
if(adjList[i].in == 0) {
stack.push(i);
}
}
for (int i = 0; i < adjList.length; i++) {
etv[i] = 0;
}
while(!stack.isEmpty()) {
gettop = (int) stack.pop();
count++;
stack2.push(gettop);
for (e = adjList[gettop].firstedge; e != null; e = e.next) {
k = e.adjevex;
if((--adjList[k].in) == 0) {
stack.push(k);
}
if(etv[gettop] + e.weight > etv[k]) {
etv[k] = etv[gettop] + e.weight;
}
}
}
if(count < adjList.length) return false;
else return true;
}
關鍵路徑算法:
public void CriticalPath() {
EdgeNode e;
int gettop, k, j;
int ete, lte;
if(!this.ToplogicalSort()) {
System.out.println("該網中存在回路!");
return;
}
ltv = new int[adjList.length];
for (int i = 0; i < adjList.length; i++) {
ltv[i] = etv[etv.length - 1];
}
while(!stack2.isEmpty()) {
gettop = (int) stack2.pop();
for(e = adjList[gettop].firstedge; e != null; e = e.next) {
k = e.adjevex;
if(ltv[k] - e.weight < ltv[gettop]) {
ltv[gettop] = ltv[k] - e.weight;
}
}
}
for (int i = 0; i < adjList.length; i++) {
for(e = adjList[i].firstedge; e != null; e = e.next) {
k = e.adjevex;
ete = etv[i];
lte = ltv[k] - e.weight;
if(ete == lte) {
System.out.print("<" + adjList[i].data + "," + adjList[k].data + "> length: " + e.weight + ",");
}
}
}
}
完整代碼:
public class CriticalPathSort {
int[] etv, ltv;
Stack stack = new Stack(); //存儲入度為0的頂點,便於每次尋找入度為0的頂點時都遍歷整個鄰接表
Stack stack2 = new Stack(); //將頂點序號壓入拓撲序列的棧
static VertexNode[] adjList;
public boolean ToplogicalSort() {
EdgeNode e;
int k, gettop;
int count = 0;
etv = new int[adjList.length];
for (int i = 0; i < adjList.length; i++) {
if(adjList[i].in == 0) {
stack.push(i);
}
}
for (int i = 0; i < adjList.length; i++) {
etv[i] = 0;
}
while(!stack.isEmpty()) {
gettop = (int) stack.pop();
count++;
stack2.push(gettop);
for (e = adjList[gettop].firstedge; e != null; e = e.next) {
k = e.adjevex;
if((--adjList[k].in) == 0) {
stack.push(k);
}
if(etv[gettop] + e.weight > etv[k]) {
etv[k] = etv[gettop] + e.weight;
}
}
}
if(count < adjList.length) return false;
else return true;
}
public void CriticalPath() {
EdgeNode e;
int gettop, k, j;
int ete, lte;
if(!this.ToplogicalSort()) {
System.out.println("該網中存在回路!");
return;
}
ltv = new int[adjList.length];
for (int i = 0; i < adjList.length; i++) {
ltv[i] = etv[etv.length - 1];
}
while(!stack2.isEmpty()) {
gettop = (int) stack2.pop();
for(e = adjList[gettop].firstedge; e != null; e = e.next) {
k = e.adjevex;
if(ltv[k] - e.weight < ltv[gettop]) {
ltv[gettop] = ltv[k] - e.weight;
}
}
}
for (int i = 0; i < adjList.length; i++) {
for(e = adjList[i].firstedge; e != null; e = e.next) {
k = e.adjevex;
ete = etv[i];
lte = ltv[k] - e.weight;
if(ete == lte) {
System.out.print("<" + adjList[i].data + "," + adjList[k].data + "> length: " + e.weight + ",");
}
}
}
}
public static EdgeNode getAdjvex(VertexNode node) {
EdgeNode e = node.firstedge;
while(e != null) {
if(e.next == null) break;
else
e = e.next;
}
return e;
}
public static void main(String[] args) {
int[] ins = {0, 1, 1, 2, 2, 1, 1, 2, 1, 2};
int[][] adjvexs = {
{2, 1},
{4, 3},
{5, 3},
{4},
{7, 6},
{7},
{9},
{8},
{9},
{}
};
int[][] widths = {
{4, 3},
{6, 5},
{7, 8},
{3},
{4, 9},
{6},
{2},
{5},
{3},
{}
};
adjList = new VertexNode[ins.length];
for (int i = 0; i < ins.length; i++) {
adjList[i] = new VertexNode("V"+i, ins[i],null);
if(adjvexs[i].length > 0) {
for (int j = 0; j < adjvexs[i].length; j++) {
if(adjList[i].firstedge == null)
adjList[i].firstedge = new EdgeNode(adjvexs[i][j], widths[i][j], null);
else {
getAdjvex(adjList[i]).next = new EdgeNode(adjvexs[i][j], widths[i][j], null);
}
}
}
}
CriticalPathSort c = new CriticalPathSort();
c.CriticalPath();
}
}
注意:這個例子中只有唯一一條關鍵路徑,這並不表示不存在多條關鍵路徑的有向無環圖。如果是多條關鍵路徑,則單是提高一條關鍵路徑上的關鍵活動速度並不能導致整個工期縮短,而必須提高同時在幾條關鍵路徑上的活動的速度。