關鍵路徑(Critical Path)


引入

拓撲排序主要是為解決一個工程能否順序進行的問題,但有時還需要解決工程完成需要的最短時間問題。這時僅僅是拓撲排序是不夠的。

通過拓撲排序,可以有效地分析出一個有向圖是否存在環;若不存在,那它的拓撲排序是什么?另一方面,利用求關鍵路徑的算法,可以得到完成工程的最短工期及關鍵活動有哪些。(摘自《大話數據結構》)

定義

事件:開始XX事情 | 完成XX事情
活動:做XX事情
源點:某流程的開始
匯點:某流程的結束
AOE網:在一個表示工程的帶權有向圖中,用頂點表示事件,用有向邊表示活動,用邊上的權值表示活動的持續時間,這種有向圖的邊表示活動的網,我們稱之為AOE網(Activity On Edge Network)。

把路徑上各個活動所持續的時間之和稱為路徑長度,從源點到匯點具有最大長度的路徑叫關鍵路徑,在關鍵路徑上的活動叫關鍵活動。(摘自《大話數據結構》)事件是一個點,活動是一條線。

舉例

以炒一盤肉作為示例:將炒一盤肉比作一個工程。一位大人負責廚房的事情,一位小孩負責從院子里摘菜並送到廚房。小孩從院子里摘菜並送到廚房需要1.5分鍾。大人准備肉需要7分鍾;准備菜需要1.5分鍾;炒肉需要2分鍾;炒菜需要1分鍾。

問題:這項工程的最短時間為多少?

顯然,該工程的流程不是這樣:摘菜送到廚房(1.5分鍾)->准備肉(7分鍾)->准備菜(1.5分鍾)->炒肉(2分鍾)->炒菜(1分鍾),共13分鍾。因為小孩摘菜的同時大人可以准備肉;在炒肉的后面階段可以將菜放入鍋里一起炒,這也符合日常實踐。而准備菜這個活動又受到“摘菜並送到廚房”和“准備肉”這兩個活動的制約:先有菜才能對菜進行處理!先處理好肉才能處理菜吧,廚房里只有一個人。

用前面的“事件”、“活動”、“源點”和“匯點”來表示這個工程。

源點:開始准備肉/開始摘菜送到廚房
匯點:完成炒菜/完成炒肉
事件

開始摘菜並送到廚房>完成摘菜並送到廚房
開始准備肉>完成准備肉
開始准備菜>完成准備菜
開始炒肉>完成炒肉
開始炒菜>完成炒菜

約束條件:“完成准備肉”和“完成摘菜並送到廚房”才能“開始准備菜”。

活動

摘菜送到廚房(歷時1.5分鍾)
准備肉(歷時7分鍾)
准備菜(歷時1.5分鍾)
炒肉(歷時2分鍾)
炒菜(歷時1分鍾)

若將此工程繪成有向圖,可表示如下:

炒肉工程圖

問題1(引入etv):各個事件(頂點)的最早開始時間是多少?引入詞匯Earliest Time of Vertex(etv)來表示最早開始時間。Etv[0]表示頂點v0的最早開始時間。

炒肉工程圖ETV

問題1總結:
etv[k]=0, 當k=0時。
etv[k]=max{etv[i]+len<vi, vk>},當k≠0且<vi, vk>∈P[k]時,其中P[k]是到達頂點vk的弧的集合。

對結果數據含義的解釋:
etv[0]:開始准備肉/開始摘菜並送菜最早於第0分鍾發生。
etv[1]:完成准備肉/開始准備菜/開始炒肉最早於第7分鍾發生。
etv[2]:完成摘菜並送菜/開始准備菜最早於第1.5分鍾發生。
etv[3]:完成准備菜/開始炒菜最早於第8.5分鍾發生。
etv[4]:完成炒肉/完成炒菜最早於第9分鍾發生。

問題2(引入ltv):各個事件(頂點)的最晚開始時間是多少?引入詞匯Latest Time of Vertex(Ltv)表示最晚開始時間。Ltv[0]表示頂點v0的最晚開始時間。

炒肉工程圖LTV

問題2總結:

ltv[k] = etv[k],當k=n-1時(n為圖G的頂點數)
ltv[k] = min{etv[i]-len<vk, vi>}當k<n-1且<vk, vi>∈S[k]其中S[k]是從vk出發的弧的集合。len<vk, vi>是弧<vk, vi>的權值。

對結果數據含義的解釋:
ltv[0]:開始准備肉/開始摘菜最晚於第0分鍾發生。
ltv[1]:完成准備肉/開始准備菜最晚於第7分鍾發生。
ltv[2]:完成摘菜並送菜/開始准備菜最晚於第7分鍾發生。
ltv[3]:完成准備菜/開始炒菜最晚於第8.5分鍾發生。
ltv[4]:完成炒肉/完成炒菜最晚於第9.5分鍾發生。

問題3(引入問題):完成此工程的最短時間是多少?由於ltv[4]=9.5,按其含義即可知最晚於9.5分鍾完成工程。

問題4(引入ete/lte):完成整個工程的過程中,有哪些活動是對整個工程的時間起決定作用的?

觀察:

0 1 2 3 4
etv 0 7 1.5 8.5 9.5
ltv 0 7 7 8.5 9.5

頂點v0,v1,v3,v4的etv與ltv,意味着這些頂點代表的事件的最早開始時間和最晚開始時間相等,即這些事件是刻不容緩的。所以是不是將這些頂點連接起來所形成的路徑就是完成整個工程所需要經過的路徑呢?看起來有點像:

v4之前的頂點有v1和v3且他們的etv和ltv都相等,所以路徑為v1->v4,v3->v1嗎?v3和v1的etv和ltv也相等所以,v1->v3也是所求路徑?v0和v1的etv和ltv也相等,那么v0->v1也是?

回到本文開頭。用弧表示活動,即弧(邊)表示做某事,如“炒肉”這件活動。那么,上圖中一共有6條弧(邊),哪些弧所代表的活動是最刻不容緩的?什么條件滿足才能顯示出此活動刻不容緩?

為此引入詞匯:“活動最早開始時間”(Earliest Time of Edge)和“活動最晚開始時間”(Latest Time of Edge)。

計算各弧(邊)的Ete和Lte

以v0點為起點,對於弧(邊)<v0, v2>,etv[0]=0,ltv[2]=7,len<v0, v2>=1.5,那么弧<v0, v2>的ete為ete=etv[0]=0,從事件v0開始的活動,自然最早也只能是etv[0]這個時刻開始。而弧<v0, v2>的lte為lte=ltv[2]-len<v0, v2>=7-1.5=5.5即<v0, v2>這個活動(摘菜並送到廚房)最晚不能超過5.5才能開始。小結:<v0, v2>弧的ete=0,lte=5.5。

以v0點為起點,對於弧(邊)<v0, v1>,etv[0]=0,ltv[1]=7,len<v0, v1>=7,那么弧<v0, v1>的ete為ete=etv[0]=0,從事件v0開始的活動,自然最早也只能是etv[0]這個時刻開始。而弧<v0, v1>的lte為lte=ltv[1]-len<v0, v1>=7-7=0即<v0, v1>這個活動(准備肉)最晚不能超過0才開始。小結:<v0, v1>弧的ete=0,lte=0。所以<v0, v1>這個弧是刻不容緩的。

同樣地,
以v1為起點:
<v1, v3>,ete=etv[1]=7,lte=ltv[3]-len<v1, v3>=8.5-1.5=7
<v1, v4>,ete=etv[1]=7,lte=ltv[4]-len<v1, v4>=9.5-2=7.5
小結:<v1, v3>這個活動刻不容緩。

以v2為起點:
<v2, v3>,ete=etv[2]=1.5,lte=ltv[3]-len<v2, v3>=8.5-1.5=7
解釋:准備菜最早可從1.5分鍾開始,最晚從7分鍾開始。

以v3為起點:
<v3, v4>,ete=etv[3]=8.5,lte=ltv[4]-len<v3, v4>=9.5-1=8.5
小結:<v1, v3>這個活動刻不容緩。

總結:以下這些弧所代表的活動會影響到整個工程的完成事件。若優化他們,則可改進整個工程的完成事件。
<v0, v1>、<v1, v3>、<v3, v4>

對於頂點vi,若其出邊的弧頭頂點為vk則ete=etv[i],lte=ltv[k]-len<vi, vk>。若ete與lte相等,則<vi, vk>是關鍵路徑上的弧。關鍵路徑不止一條。如:若上圖的每條邊的ete與lte都相等,那么關鍵路徑就不止一條。要優化所有這些路徑才能優化整個工程(圖)的最短時間。

炒肉工程圖ETE和LTE

如何求得關鍵路徑(引入拓撲排序

完成一個工程,其中的事件是有先后順序的,后面的事件依賴前面的事件。對於整個流程,若任意事件(vi, vj)構成路徑,則vi一定在vj之前。這其實就是對事件(頂點)進行拓撲排序。

從而可知求關鍵路徑的大致流程為:

①拓撲排序。
②求得etv數組。
③逆向地從末頂點開始求ltv數組。
④從首頂點開始遍歷各頂點求ete和lte,若ete與lte相等,則該弧(邊)為關鍵路徑上的弧(邊)。

給弧(邊)類增加屬性Weight(權值)、Ete(活動最早開始時間)、Lte(活動最晚開始時間)。

給事件(頂點)類增加屬性Etv(事件最早開始事件)、Ltv(事件最晚開始時間)。

計算過程

1.用鄰接表來表示有向圖G。計數器counter置0。
2.將圖G中的所有入度為0的頂點入棧S1。
3.新建數組Etv用於存儲各頂點的Etv值,顯然數組Etv長度與頂點數一樣。
4.創建棧S2用於緩存S1中彈出的入度為0的頂點,待計算Ltv使用。
5.Etv數組各元素置0。
6.若棧S1不為空,則從棧中彈出一個頂點,記作vtop
7.將vtop入棧S2.
8.計數器counter++。
9.將頂點vtop的出邊弧頭頂點vadj的入度減1。
10.求vtop.etv的值:如果vadj.etv < vtop.etv + len<vtop, vadj>即vtop.etv < vtop.etv + e.Weight則vtop.etv = vtop.etv + e.Weight。
11.如果vadj的入度為0,則將vadj入棧S1。
12.跳至步驟6。
13.如果計數器counter與圖G的頂點數相等,則完成拓撲排序,否則意味着圖G中有環,不能進行拓撲排序。
14.創建數組Ltv用於存儲各頂點的Ltv值,自然數組Ltv長度與頂點數一樣。
15.Ltv數組各元素的值設置為etv[n-1],其中n為圖G的頂點數目。
16.若棧S2不為空,則從棧中彈出一個頂點,記作vtop
17.遍歷頂點vtop的各出邊弧e(及弧e的弧頭vadj)。
18.若vtop.ltv > vadj.ltv - len<vtop, vadj>則vtop.ltv= vadj.ltv - len<vtop, vadj>。
19.跳至步驟16。
20.遍歷圖G的頂點,計算各頂點的出邊(弧)的Ete和Lte。
21.對於頂點vk其出邊的Ete=etv[k];其出邊的Lte=ltv[j] - len<vk, vj>=ltv[j]-e.Weight。
22.若Lte與Ete相等,則輸出邊或弧<vk, vj>

復雜度

拓撲排序及求Etv的時間復雜度為O(n+e),求Ltv的時間復雜度也為O(n+e),求Ete和Lte的時間復雜度也為O(n+e)。根據對時間復雜度的定義,常數系數可以忽略,所以整個求關鍵路徑算法的時間復雜度依然是O(n+e)。其中n是頂點數目,e是弧(邊)的數目。

代碼

用鄰接矩陣來表示圖G。如前面的圖。以及大話數據結構上的圖,如下圖:

炒肉工程圖ETE和LTE

C#代碼

using System;
using System.Collections.Generic;

namespace CriticalPath
{
    class Program
    {
        static void Main(string[] args)
        {
            CriticalPath(new Vertex[] {
                new Vertex(0,"開始",new Edge[]{new Edge(){ Vertex = 1, Weight = 70, Data="准備肉"}, new Edge(){ Vertex = 2, Weight = 15, Data = "摘送菜" } }),
                //new Vertex(2,"准備蒜苗",new Edge[]{ new Edge() { Vertex = 4, Weight = 10, Data = "准備蒜苗" }, new Edge(){Vertex = 1, Weight = 10, Data = ""} }), // 讓圖中存在環
                new Vertex(1,"完成准備肉",new Edge[]{new Edge(){ Vertex = 3, Weight = 15, Data="准備菜"}, new Edge(){ Vertex = 4, Weight = 20, Data="炒肉" } }),
                new Vertex(1,"完成摘菜送菜", new Edge[]{new Edge(){ Vertex = 3, Weight = 15, Data="准備菜"}}),
                new Vertex(2,"完成准備菜",new Edge[]{new Edge(){ Vertex = 4, Weight = 10, Data="炒菜"}}),
                new Vertex(2,"完成",new Edge[]{}),
            });

            Console.WriteLine("來自《大話數據結構》P281的數據。");

            // 《大話數據結構》P281的圖7-9-4的數據。
            CriticalPath(new Vertex[] {
                new Vertex(0,"0",new Edge[]{new Edge(){ Vertex = 2, Weight = 4, Data="A1"}, new Edge(){ Vertex = 1, Weight = 3, Data = "A0" } }),
                new Vertex(1,"1",new Edge[]{new Edge(){ Vertex = 4, Weight = 6, Data="A3"}, new Edge(){ Vertex = 3, Weight = 5, Data="A2"}}),
                new Vertex(1,"2",new Edge[]{new Edge(){ Vertex = 5, Weight = 7, Data="A5"},new Edge(){ Vertex = 3, Weight = 8, Data="A4"}}),
                new Vertex(2,"3",new Edge[]{new Edge(){ Vertex = 4, Weight = 3, Data="A6"}}),
                new Vertex(2,"4",new Edge[]{new Edge(){ Vertex = 7, Weight = 4, Data="A8"},new Edge(){ Vertex = 6, Weight = 9, Data="A7"}}),
                new Vertex(1,"5",new Edge[]{new Edge(){ Vertex = 7, Weight = 6, Data="A9"}}),
                new Vertex(1,"6",new Edge[]{new Edge(){ Vertex = 9, Weight = 2, Data="A10"}}),
                new Vertex(2,"7",new Edge[]{new Edge(){ Vertex = 8, Weight = 5, Data="A11"}}),
                new Vertex(1,"8",new Edge[]{new Edge(){ Vertex = 9, Weight = 3, Data="A12"}}),
                new Vertex(2,"9",new Edge[]{}),
            });
        }

        public static void CriticalPath(Vertex[] graph)
        {
            List<string> results = new List<string>();

            // 拓撲排序的同時計算Etv。

            // 1.用於判斷最終圖中是否還有入度為0的頂點。若沒有則拓撲排序成功。否則沒有。
            int counter = 0;
            // 緩存入度為0的頂點。
            Stack<Vertex> s1 = new Stack<Vertex>();
            // 2.將所有入度為0的頂點入棧S1。
            for (int i = 0; i < graph.Length; i++)
            {
                Vertex v = graph[i];

                if (v.InDegree == 0)
                {
                    s1.Push(v);
                }
            }
            // 3.新建數組etv用於存儲各頂點的etv值。顯然,數組長度與頂點數一樣。
            //int[] etv = new int[graph.Length];
            // 4.etv數組各元素置0。
            for (int i = 0; i < graph.Length; i++)
            {
                graph[i].Etv = 0;
            }
            // 5.創建棧S2用於緩存S1中彈出的入度為0的頂點,之后用於求各頂點的ltv。
            Stack<Vertex> s2 = new Stack<Vertex>();
            // 6.若棧S1不為空,
            while (s1.Count != 0)
            {
                // 則從棧中彈出一個頂點Vtop。
                Vertex top = s1.Pop();
                // 7.將Vtop入棧S2.
                s2.Push(top);
                // 8.計數器遞增。
                counter++;
                // 9.將頂點Vtop的出邊弧頭Vadj的入度減1。
                for (var e = top.Edge; e != null; e = e.Next)
                {
                    // 弧頭頂點在頂點數組中的索引。
                    int n = e.Vertex;

                    Vertex adj = graph[n];
                    // 頂點Vadj的入度減1。
                    adj.InDegree--;
                    // 10.求頂點Vadj對應的etv的值。
                    if (adj.Etv < top.Etv + e.Weight)
                    {
                        adj.Etv = top.Etv + e.Weight;
                    }
                    // 11.如果Vadj的入度為0,
                    if (adj.InDegree == 0)
                    {
                        // 則將Vadj入棧S1。
                        s1.Push(adj);
                    }
                }
                // 12.跳至步驟6。
            }

            // 13.如果計數器counter與圖G的頂點數相等,則完成拓撲排序,否則意味着圖G中有環,不能進行拓撲排序。
            if (counter != graph.Length)
            {
                throw new Exception($"錯誤:圖G中頂點數:{graph.Length},還剩{graph.Length - counter}個頂點入度非0。");
            }
            //else // 此程序不是為了求拓撲排序,所以不輸出拓撲排序的結果。
            //{
            //    Console.WriteLine(string.Join(",", result.ToArray()));
            //}

            // 准備計算Ltv。

            // 14.創建數組ltv用於存儲各頂點的ltv,顯然數組ltv長度與頂點數一樣。
            //int[] ltv = new int[graph.Length];
            // 15.ltv數組各元素設置為etv[n-1],其中n為圖G的頂點數目。
            for (int i = graph.Length - 1, n = graph.Length; i > 0; i--)
            {
                graph[i].Ltv = graph[n - 1].Etv;
            }
            // 16.若S2不為空,則從棧中彈出一個頂點Vtop。
            while (s2.Count != 0)
            {
                Vertex top = s2.Pop();

                // 17.遍歷頂點Vtop的各出邊弧e(及弧e的弧頭Vadj)
                for (var e = top.Edge; e != null; e = e.Next)
                {
                    int n = e.Vertex;

                    Vertex adj = graph[n];
                    // 18.若top.Ltv > adj.Ltv - Len(Vtop, Vadj),則top.Ltv為adj.Ltv - Len(Vtop, Vadj)。
                    if (top.Ltv > adj.Ltv - e.Weight)
                    {
                        top.Ltv = adj.Ltv - e.Weight;
                    }
                }
                // 19.跳至步驟16。
            }
            // 20.遍歷圖G的頂點,計算各頂點的出邊(弧)的ete和lte。
            for (int i = 0; i < graph.Length; i++)
            {
                Vertex vk = graph[i];

                // 21.對於頂點Vk,其出邊的ete=etv[k];lte=ltv[j] - len<Vk, Vj> = ltv[j] - e.weight。
                for (var e = vk.Edge; e != null; e = e.Next)
                {
                    Vertex vj = graph[e.Vertex];

                    e.Ete = vk.Etv;
                    e.Lte = vj.Ltv - e.Weight;
                    // 22.若lte與ete相等,則輸出邊或弧<Vk, Vj>。
                    if (e.Ete == e.Lte)
                    {
                        results.Add($"關鍵路徑:\t{e.Data}\t的最早開始時間:{e.Ete},\t最晚開始時間:{e.Lte},\t耗時:{e.Weight}");
                    }
                    else
                    {
                        results.Add($"非關鍵路徑:\t{e.Data}\t的最早開始時間:{e.Ete},\t最晚開始時間:{e.Lte},\t耗時:{e.Weight}");
                    }
                }
            }
            results.Sort();
            Console.WriteLine(string.Join('\n', results.ToArray()));
        }
    }

    /// <summary>
    /// 圖G的頂點。用鄰接表來表示頂點的出邊。
    /// </summary>
    public class Vertex
    {
        /// <summary>
        /// 入度。
        /// </summary>
        public int InDegree { get; set; } = 0;
        /// <summary>
        /// 存儲的數據。
        /// </summary>
        public string Data { get; set; } = "";
        /// <summary>
        /// 出邊。
        /// </summary>
        public Edge Edge { get; set; } = null;
        /// <summary>
        /// 事件的最早開始時間。
        /// </summary>
        public int Etv { get; set; } = 0;
        /// <summary>
        /// 事件的最晚開始時間。
        /// </summary>
        public int Ltv { get; set; } = 0;

        public Vertex(int inDegree, string data, Edge[] adjacentEdges)
        {
            this.InDegree = inDegree;
            this.Data = data;

            Edge e = null;

            for (int i = 0; i < adjacentEdges.Length; i++)
            {
                if (e == null)
                {
                    e = new Edge(adjacentEdges[i].Vertex, adjacentEdges[i].Weight, adjacentEdges[i].Data, null);
                    this.Edge = e;
                }
                else
                {
                    e.Next = new Edge(adjacentEdges[i].Vertex, adjacentEdges[i].Weight, adjacentEdges[i].Data, null);
                    e = e.Next;
                }
            }
        }
    }

    /// <summary>
    /// 圖G的邊。以鄰接表表示頂點的出邊。
    /// </summary>
    public class Edge
    {
        /// <summary>
        /// 邊的弧頭頂點在頂點數組中的索引。
        /// </summary>
        public int Vertex { get; set; } = -1;
        /// <summary>
        /// 邊的弧尾頂點的下一條出邊。
        /// </summary>
        public Edge Next { get; set; } = null;
        /// <summary>
        /// 邊的權重。
        /// </summary>
        public int Weight { get; set; } = 0;
        /// <summary>
        /// 事件的描述。
        /// </summary>
        public string Data { get; set; } = string.Empty;
        /// <summary>
        /// Earliest Time of Edge.(事件的最早發生時間。)
        /// </summary>
        public int Ete { get; set; }
        /// <summary>
        /// Latest Time of Edge.(事件的最晚發生時間。)
        /// </summary>
        public int Lte { get; set; }

        public Edge(int vertex = -1, int weight = 0, string data = "", Edge edge = null)
        {
            this.Vertex = vertex;
            this.Next = edge;
            this.Weight = weight;
            this.Data = data;
        }
    }
}

/**
運行結果:

非關鍵路徑:    炒肉    的最早開始時間:70,    最晚開始時間:75,      耗時:20
非關鍵路徑:    摘送菜  的最早開始時間:0,     最晚開始時間:55,      耗時:15
非關鍵路徑:    准備菜  的最早開始時間:15,    最晚開始時間:70,      耗時:15
關鍵路徑:      炒菜    的最早開始時間:85,    最晚開始時間:85,      耗時:10
關鍵路徑:      准備菜  的最早開始時間:70,    最晚開始時間:70,      耗時:15
關鍵路徑:      准備肉  的最早開始時間:0,     最晚開始時間:0,       耗時:70
來自《大話數據結構》P281的數據。
非關鍵路徑:    A0      的最早開始時間:0,     最晚開始時間:4,       耗時:3
非關鍵路徑:    A10     的最早開始時間:24,    最晚開始時間:25,      耗時:2
非關鍵路徑:    A2      的最早開始時間:3,     最晚開始時間:7,       耗時:5
非關鍵路徑:    A3      的最早開始時間:3,     最晚開始時間:9,       耗時:6
非關鍵路徑:    A5      的最早開始時間:4,     最晚開始時間:6,       耗時:7
非關鍵路徑:    A7      的最早開始時間:15,    最晚開始時間:16,      耗時:9
非關鍵路徑:    A9      的最早開始時間:11,    最晚開始時間:13,      耗時:6
關鍵路徑:      A1      的最早開始時間:0,     最晚開始時間:0,       耗時:4
關鍵路徑:      A11     的最早開始時間:19,    最晚開始時間:19,      耗時:5
關鍵路徑:      A12     的最早開始時間:24,    最晚開始時間:24,      耗時:3
關鍵路徑:      A4      的最早開始時間:4,     最晚開始時間:4,       耗時:8
關鍵路徑:      A6      的最早開始時間:12,    最晚開始時間:12,      耗時:3
關鍵路徑:      A8      的最早開始時間:15,    最晚開始時間:15,      耗時:4
 */

TypeScript代碼


/**
 * 圖G的頂點。用鄰接表來表示頂點的出邊。
 */
class Vertex {
    // 入度。
    InDegree: number = 0;
    // 存儲的數據。
    Data: string = "";
    // 首條出邊。
    Edge: Edge = null;
    // 事件的最早開始時間。
    Etv: number = 0;
    // 事件的最晚開始時間。
    Ltv: number = 0;

    /**
     * 創建鄰接表的一行。
     * @param inDegree 頂點的入度。
     * @param data 頂點存儲的數據。
     * @param adjacentEdges 從該頂點出發的弧。
     */
    constructor(inDegree: number, data: string, adjacentEdges: Edge[]) {
        this.InDegree = inDegree;
        this.Data = data;

        let e: Edge = null;

        for (let i = 0; i < adjacentEdges.length; i++) {

            if (e === null) {
                e = new Edge(adjacentEdges[i].Vertex, adjacentEdges[i].Weight, adjacentEdges[i].Data, null);
                this.Edge = e;
            }
            else {
                e.Next = new Edge(adjacentEdges[i].Vertex, adjacentEdges[i].Weight, adjacentEdges[i].Data, null);
                e = e.Next;
            }
        }
    }
}

/**
 * 圖G的邊。以鄰接表表示頂點的出邊。
 */
class Edge {
    // 邊的弧頭頂點在頂點數組中的索引。
    Vertex: number = -1;
    // 邊的弧尾頂點的下一條出邊。
    Next: Edge = null;
    // 邊的權重。
    Weight: number = 0;
    // 事件的描述。
    Data: string = "";
    // Earlist Time of Edge.(事件的最早發生時間。)
    Ete: number = 0;
    // Latest Time of Edge.(事件的最晚發生時間。)
    Lte: number = 0;

    /**
     * 創建鄰接表中的一條邊/弧。
     * @param vertex 鄰接表中,該邊的弧尾頂點的在頂點數組中的下標。
     * @param weight 弧的權重。
     * @param data 事件的描述。
     * @param next 鄰接表中,該邊的弧尾頂點的下一條出邊。 
     */
    constructor(vertex: number = -1, weight: number = 0, data: string = "", next: Edge = null) {
        this.Vertex = vertex;
        this.Next = next;
        this.Weight = weight;
        this.Data = data;
    }
}

function criticalPath(graph: Vertex[]) {
    let results: string[] = [];

    // 拓撲排序的同時計算Etv。

    // 1.用於判斷最終圖中是否還有入度為0的頂點。若沒有則拓撲排序成功。否則沒有。
    let counter: number = 0;
    // 緩存入度為0的頂點。
    let s1: Vertex[] = [];
    // 2.將所有入度為0的頂點入棧S1。
    for (let i = 0; i < graph.length; i++) {
        let v: Vertex = graph[i];

        if (v.InDegree === 0) {
            s1.push(v);
        }
    }
    // 3.新建數組etv用於存儲各頂點的etv值。顯然,數組長度與頂點數一樣。
    // let etv:number[] = [];
    // 4.etv數組各元素置0。
    for (let i = 0; i < graph.length; i++) {
        graph[i].Etv = 0;
    }
    // 5.創建棧S2用於緩存S1中彈出的入度為0的頂點,之后用於求各頂點的Ltv。
    let s2: Vertex[] = [];
    // 6.若棧S1不為空,
    while (s1.length != 0) {
        // 則從棧中彈出一個頂點Vtop。
        let top: Vertex = s1.pop();
        // 7.將Vtop入棧S2。
        s2.push(top);
        // 8.計數器遞增。
        counter++;
        // 9.將頂點Vtop的出邊弧頭Vadj的入度減1。
        for (let e = top.Edge; e != null; e = e.Next) {
            // 弧頭頂點在頂點數組中的索引。
            let n: number = e.Vertex;

            let adj: Vertex = graph[n];
            // 頂點Vadj的入度減1。
            adj.InDegree--;
            // 10.求頂點Vadj對應的etv的值。
            if (adj.Etv < top.Etv + e.Weight) {
                adj.Etv = top.Etv + e.Weight;
            }
            // 11.如果Vadj的入度為0,
            if (adj.InDegree == 0) {
                // 則將Vadj入棧S1。
                s1.push(adj);
            }
        }
        // 12.跳至步驟6。
    }
    // 13.如果計數器counter與圖G的頂點數相等,則完成拓撲排序,否則意味着圖G中有環,不能進行拓撲排序。
    if (counter != graph.length) {
        throw new Error(`錯誤:圖G中頂點數:${graph.length},還剩${graph.length - counter}個頂點入度非0。`);
    }
    // 14.創建數組ltv用於存儲各頂點的ltv,顯然數組ltv長度與頂點數一樣。
    // let ltv:number[] = [];
    // 15.ltv數組各元素設置為etv[n-1],其中n為圖G的頂點數目。
    for (let i: number = graph.length - 1, n: number = graph.length; i > 0; i--) {
        graph[i].Ltv = graph[n - 1].Etv;
    }
    // 16.若S2不為空,則從棧中彈出一個頂點Vtop。
    while (s2.length != 0) {
        let top: Vertex = s2.pop();

        // 17.遍歷頂點Vtop的各出邊弧e(及弧e的弧頭Vadj)
        for (let e = top.Edge; e != null; e = e.Next) {
            let n: number = e.Vertex;

            let adj: Vertex = graph[n];
            // 18.若top.Ltv > adj.Ltv - Len(Vtop, Vadj),則top.Ltv為adj.Ltv - Len(Vtop, Vadj)。
            if (top.Ltv > adj.Ltv - e.Weight) {
                top.Ltv = adj.Ltv - e.Weight;
            }
        }
        // 19.跳至步驟16。
    }
    // 20.遍歷圖G的頂點,計算各頂點的出邊(弧)的ete和lte。
    for (let i: number = 0; i < graph.length; i++) {
        let vk: Vertex = graph[i];

        // 21.對於頂點Vk,其出邊的ete=etv[k];lte=ltv[j] - len<Vk, Vj> = ltv[j] - e.weight。
        for (let e: Edge = vk.Edge; e != null; e = e.Next) {
            let vj: Vertex = graph[e.Vertex];

            e.Ete = vk.Etv;
            e.Lte = vj.Ltv - e.Weight;
            // 22.若lte與ete相等,則輸出邊或弧<Vk, Vj>。
            if (e.Ete == e.Lte) {
                results.push(`關鍵路徑:\t${e.Data}\t的最早開始時間:${e.Ete},\t最晚開始時間:${e.Lte},\t耗時:${e.Weight}`);
            }
            else {
                results.push(`非關鍵路徑:\t${e.Data}\t的最早開始時間:${e.Ete},\t最晚開始時間:${e.Lte},\t耗時:${e.Weight}`);
            }
        }
    }
    results.sort();
    console.log(results.join('\n'));//string.Join('\n', results.ToArray()));
}

function main() {
    criticalPath([
        new Vertex(0, "開始", [new Edge(1, 70, "准備肉"), new Edge(2, 15, "摘送菜")]),
        //new Vertex(2,"准備蒜苗",[new Edge(4, 10, "准備蒜苗"), new Edge(1, 10, "") ]), // 讓圖中存在環
        new Vertex(1, "完成准備肉", [new Edge(3, 15, "准備菜"), new Edge(4, 20, "炒肉")]),
        new Vertex(1, "完成摘菜送菜", [new Edge(3, 15, "准備菜")]),
        new Vertex(2, "完成准備菜", [new Edge(4, 10, "炒菜")]),
        new Vertex(2, "完成", [])
    ]);

    console.log("來自《大話數據結構》P281的數據。");

    criticalPath([
        new Vertex(0, "0", [new Edge(2, 4, "A1"), new Edge(1, 3, "A0")]),
        new Vertex(1, "1", [new Edge(4, 6, "A3"), new Edge(3, 5, "A2")]),
        new Vertex(1, "2", [new Edge(5, 7, "A5"), new Edge(3, 8, "A4")]),
        new Vertex(2, "3", [new Edge(4, 3, "A6")]),
        new Vertex(2, "4", [new Edge(7, 4, "A8"), new Edge(6, 9, "A7")]),
        new Vertex(1, "5", [new Edge(7, 6, "A9")]),
        new Vertex(1, "6", [new Edge(9, 2, "A10")]),
        new Vertex(2, "7", [new Edge(8, 5, "A11")]),
        new Vertex(1, "8", [new Edge(9, 3, "A12")]),
        new Vertex(2, "9", []),
    ]);
}

main();

/**
運行結果:

關鍵路徑:      准備肉  的最早開始時間:0,     最晚開始時間:0,       耗時:70
關鍵路徑:      准備菜  的最早開始時間:70,    最晚開始時間:70,      耗時:15
關鍵路徑:      炒菜    的最早開始時間:85,    最晚開始時間:85,      耗時:10
非關鍵路徑:    准備菜  的最早開始時間:15,    最晚開始時間:70,      耗時:15
非關鍵路徑:    摘送菜  的最早開始時間:0,     最晚開始時間:55,      耗時:15
非關鍵路徑:    炒肉    的最早開始時間:70,    最晚開始時間:75,      耗時:20
來自《大話數據結構》P281的數據。
關鍵路徑:      A1      的最早開始時間:0,     最晚開始時間:0,       耗時:4
關鍵路徑:      A11     的最早開始時間:19,    最晚開始時間:19,      耗時:5
關鍵路徑:      A12     的最早開始時間:24,    最晚開始時間:24,      耗時:3
關鍵路徑:      A4      的最早開始時間:4,     最晚開始時間:4,       耗時:8
關鍵路徑:      A6      的最早開始時間:12,    最晚開始時間:12,      耗時:3
關鍵路徑:      A8      的最早開始時間:15,    最晚開始時間:15,      耗時:4
非關鍵路徑:    A0      的最早開始時間:0,     最晚開始時間:4,       耗時:3
非關鍵路徑:    A10     的最早開始時間:24,    最晚開始時間:25,      耗時:2
非關鍵路徑:    A2      的最早開始時間:3,     最晚開始時間:7,       耗時:5
非關鍵路徑:    A3      的最早開始時間:3,     最晚開始時間:9,       耗時:6
非關鍵路徑:    A5      的最早開始時間:4,     最晚開始時間:6,       耗時:7
非關鍵路徑:    A7      的最早開始時間:15,    最晚開始時間:16,      耗時:9
非關鍵路徑:    A9      的最早開始時間:11,    最晚開始時間:13,      耗時:6
 */

參考資料

《大話數據結構》 - 程傑 著 - 清華大學出版社


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM