拓撲排序

kokiafan發表於2021-06-05

引入

把完成一件事情或一個專案當成一個工程來對待,又將其分為若干個“活動”的子工程。例如:“炒一盤肉”這個工程,可以按照先後步驟畫出以下這麼一張圖。

回鍋肉流程

把上面這張圖看成是一個表示工程的有向圖,用頂點(Vertex)表示活動(Activity),用弧(Edge)表示活動(Activity)之間的優先關係,稱這樣的圖為頂點表示活動的網(Activity On Vertex Network),即AOV網。

AOV網中弧(Edge)表示活動(Activity)之間有制約關係。另外AOV網中不能存在迴路,即A這個活動的開始條件是以A這個活動的結束為先決條件的。

定義

設G=(V, E)是具有n個頂點的有向圖,V中頂點序列v1、v2、……、vn滿足:若從頂點vi到vj有一條路徑,則vi必在頂點vj之前。則這樣的頂點序列為拓撲序列。(圖G的拓撲序列可以不止一個)

即:
①有向圖G=(V, E)中,有頂點V={v1, v2, … , vn}(前提)
②v1、v2、……、vn以某種順序構成頂點序列S,從中任取兩個相異頂點vi,vj。(前提)
③若vi到vj有路徑,且vi在vj之前。(條件)
④則頂點序列S為一個拓撲序列。(結論)

核心思想

拓撲排序解決的是“一個工程能否按順序進行”的問題。其演算法過程如下:

①用鄰接表來表示有向圖G。計數器counter置0。
②將圖G中所有入度為0的頂點入棧S。
③若棧S不為空,則從棧中彈出一個頂點vtop
④將頂點vtop的出邊弧頭頂點vadj的入度減1。
⑤如果vadj的入度為0了,則將vadj入棧S。
⑥計數器counter++。
⑦跳至步驟③。
⑧如果計數器counter與圖G的頂點數相等,則完成拓撲排序,否則以為著圖G中有環,不能進行拓撲排序。

問題:嘗試對上面的圖進行拓撲排序?

推演

使用鄰接表來表示上面的圖,如下:

回鍋肉流程鄰接表

基於前面的核心思想對排序過程進行推演如下:

初始所有入度為0的頂點:棧頂->[開始做菜]
第0次迴圈開始時棧:棧頂->[開始做菜]
開始做菜出棧:棧頂->[]
準備蒜苗入度:1-1=0。
準備蒜苗入棧。棧:棧頂->[準備蒜苗]
準備辣椒入度:1-1=0。
準備辣椒入棧。棧:棧頂->[準備辣椒,準備蒜苗]
準備豬肉入度:1-1=0。
準備豬肉入棧。棧:棧頂->[準備豬肉,準備辣椒,準備蒜苗]
第0次迴圈結束時棧:棧頂->[準備豬肉,準備辣椒,準備蒜苗]

第1次迴圈開始時棧:棧頂->[準備豬肉,準備辣椒,準備蒜苗]
準備豬肉出棧:棧頂->[準備辣椒,準備蒜苗]
炒肉入度:1-1=0。
炒肉入棧。棧:棧頂->[炒肉,準備辣椒,準備蒜苗]
第1次迴圈結束時棧:棧頂->[炒肉,準備辣椒,準備蒜苗]

第2次迴圈開始時棧:棧頂->[炒肉,準備辣椒,準備蒜苗]
炒肉出棧:棧頂->[準備辣椒,準備蒜苗]
放入蔬菜入度:2-1=1。
第2次迴圈結束時棧:棧頂->[準備辣椒,準備蒜苗]

第3次迴圈開始時棧:棧頂->[準備辣椒,準備蒜苗]
準備辣椒出棧:棧頂->[準備蒜苗]
浸泡蔬菜入度:2-1=1。
第3次迴圈結束時棧:棧頂->[準備蒜苗]

第4次迴圈開始時棧:棧頂->[準備蒜苗]
準備蒜苗出棧:棧頂->[]
浸泡蔬菜入度:1-1=0。
浸泡蔬菜入棧。棧:棧頂->[浸泡蔬菜]
第4次迴圈結束時棧:棧頂->[浸泡蔬菜]

第5次迴圈開始時棧:棧頂->[浸泡蔬菜]
浸泡蔬菜出棧:棧頂->[]
放入蔬菜入度:1-1=0。
放入蔬菜入棧。棧:棧頂->[放入蔬菜]
第5次迴圈結束時棧:棧頂->[放入蔬菜]

第6次迴圈開始時棧:棧頂->[放入蔬菜]
放入蔬菜出棧:棧頂->[]
完成入度:1-1=0。
完成入棧。棧:棧頂->[完成]
第6次迴圈結束時棧:棧頂->[完成]

第7次迴圈開始時棧:棧頂->[完成]
完成出棧:棧頂->[]
第7次迴圈結束時棧:棧頂->[]

開始做菜,準備豬肉,炒肉,準備辣椒,準備蒜苗,浸泡蔬菜,放入蔬菜,完成

程式碼

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

拓撲排序鄰接表

C#程式碼

using System;
using System.Collections.Generic;
using System.Linq;

namespace TopologicalSort
{
    class Program
    {
        static void Main(string[] args)
        {

            TopologicalSort(new Vertex[] {
                new Vertex(0,"0",new int[]{11, 5, 4 }),
                new Vertex(0,"1",new int[]{8, 4, 2 }),
                new Vertex(2,"2", new int[]{9, 6, 5 }),
                new Vertex(0,"3",new int[]{13,2 }),
                new Vertex(2,"4",new int[]{7 }),
                new Vertex(3,"5",new int[]{12,8 }),
                new Vertex(1,"6",new int[]{ 5}),
                new Vertex(2,"7",new int[]{ }),
                new Vertex(2,"8",new int[]{7 }),
                new Vertex(1,"9",new int[]{ 11,10}),
                new Vertex(1,"10",new int[]{13 }),
                new Vertex(2,"11",new int[]{ }),
                new Vertex(1,"12",new int[]{ 9}),
                new Vertex(2,"13",new int[]{ }),
            });

            TopologicalSortWithDebug(new Vertex[] {
                new Vertex(0,"開始做菜",new int[]{1, 2, 3}),
                //new Vertex(2,"準備蒜苗",new int[]{4,1}), // 讓圖中存在環
                new Vertex(1,"準備蒜苗",new int[]{4}),
                new Vertex(1,"準備辣椒", new int[]{4}),
                new Vertex(1,"準備豬肉",new int[]{5}),
                new Vertex(2,"浸泡蔬菜",new int[]{6}),
                new Vertex(1,"炒肉",new int[]{6}),
                new Vertex(2,"放入蔬菜",new int[]{7}),
                new Vertex(1,"完成",new int[]{ })
            });
        }

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

            // 1.用於判斷最終圖中是否還有入度為0的頂點。若沒有則拓撲排序成功。否則沒有。
            int counter = 0;

            // 快取入度為0的頂點。
            Stack<Vertex> s = new Stack<Vertex>();

            // 2.將所有入度為0的頂點入棧。
            for (int i = 0; i < graph.Length; i++)
            {
                Vertex v = graph[i];

                if (v.InDegree == 0)
                {
                    s.Push(v);
                }
            }

            // 3.若棧S不為空,
            while (s.Count != 0)
            {
                // 則從棧中彈出一個頂點Vtop。
                Vertex top = s.Pop();

                // 4.將頂點Vtop的出邊弧頭
                for (var e = top.Edge; e != null; e = e.Next)
                {
                    // 弧頭頂點在頂點陣列中的索引。
                    int n = e.Vertex;

                    Vertex adj = graph[n];

                    // 頂點Vadj的入度減1。
                    adj.InDegree--;

                    // 5.如果Vadj的入度為0,
                    if (adj.InDegree == 0)
                    {
                        // 則將Vadj入棧S。
                        s.Push(adj);
                    }
                }
                result.Add(top.Data);

                // 6.計數器遞增。
                counter++;

                // 7.跳至步驟3。
            }

            if (counter != graph.Length)
            {
                Console.WriteLine($"錯誤:圖G中頂點數:{graph.Length},還剩{graph.Length - counter}個頂點入度非0。");
            }
            else
            {
                Console.WriteLine(string.Join(",", result.ToArray()));
            }
        }

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

            // 1.用於判斷最終圖中是否還有入度為0的頂點。若沒有則拓撲排序成功。否則沒有。
            int counter = 0;

            // 快取入度為0的頂點。
            Stack<Vertex> s = new Stack<Vertex>();

            // 2.將所有入度為0的頂點入棧。
            for (int i = 0; i < graph.Length; i++)
            {
                Vertex v = graph[i];

                if (v.InDegree == 0)
                {
                    s.Push(v);
                }
            }

            Console.WriteLine($"初始所有入度為0的頂點:{PrintStackVertex(s)}");


            // 3.若棧S不為空,
            while (s.Count != 0)
            {
                Console.WriteLine($"第{counter}次迴圈開始時棧:{PrintStackVertex(s)}");
                // 則從棧中彈出一個頂點Vtop。
                Vertex top = s.Pop();
                Console.WriteLine($"{top.Data}出棧:{PrintStackVertex(s)}");

                // 4.將頂點Vtop的出邊弧頭
                for (var e = top.Edge; e != null; e = e.Next)
                {
                    // 弧頭頂點在頂點陣列中的索引。
                    int n = e.Vertex;

                    Vertex adj = graph[n];

                    // 頂點Vadj的入度減1。
                    //adj.InDegree--;
                    Console.WriteLine($"{adj.Data}入度:{adj.InDegree}-1={--adj.InDegree}。");

                    // 5.如果Vadj的入度為0,
                    if (adj.InDegree == 0)
                    {
                        // 則將Vadj入棧S。
                        s.Push(adj);
                        Console.WriteLine($"{adj.Data}入棧。棧:{PrintStackVertex(s)}");
                    }
                }
                result.Add(top.Data);
                Console.WriteLine($"第{counter}次迴圈結束時棧:{PrintStackVertex(s)}");

                // 6.計數器遞增。
                counter++;

                // 7.跳至步驟3。
                Console.WriteLine();
            }

            if (counter != graph.Length)
            {
                Console.WriteLine($"錯誤:圖G中頂點數:{graph.Length},還剩{graph.Length - counter}個頂點入度非0。");
            }
            else
            {
                Console.WriteLine(string.Join(",", result.ToArray()));
            }
        }

        public static string PrintStackVertex(Stack<Vertex> s)
        {
            string[] datas = s.ToArray().Select(e => e.Data).ToArray();

            return $"棧頂->[{string.Join(',', datas)}]";
        }
    }

    /// <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;

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

            Edge e = null;

            for (int i = 0; i < vertexIndexes.Length; i++)
            {
                if (e == null)
                {
                    e = new Edge(vertexIndexes[i], null);
                    this.Edge = e;
                }
                else
                {
                    e.Next = new Edge(vertexIndexes[i], 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;

        public Edge(int vertex = -1, Edge edge = null)
        {
            this.Vertex = vertex;
            this.Next = edge;
        }
    }
}

TypeScript程式碼

/**
 * 圖G的頂點。用鄰接表來表示頂點的出邊。
 */
class Vertex {
    // 入度。
    InDegree: number = 0;
    // 儲存的資料。
    Data: string = "";
    // 首條出邊。
    Edge: Edge = null;

    /**
     * 建立鄰接表的一行。
     * @param inDegree 頂點的入度。
     * @param data 頂點儲存的資料。
     * @param vertexIndexes 頂點的出邊(弧)的弧頭。
     */
    constructor(inDegree: number, data: string, vertexIndexes: number[]) {
        this.InDegree = inDegree;
        this.Data = data;

        let e: Edge = null;

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

            if (e === null) {
                e = new Edge(vertexIndexes[i], null);
                this.Edge = e;
            }
            else {

                e.Next = new Edge(vertexIndexes[i], null);
                e = e.Next;
            }
        }
    }
}

/**
 * 圖G的邊。以鄰接表表示頂點的出邊。
 */
class Edge {
    // 邊的弧頭頂點在頂點陣列中的索引。
    Vertex: number = -1;
    // 邊的弧尾頂點的下一條出邊。
    Next: Edge = null;
    /**
     * 建立鄰接表中的一條邊/弧。
     * @param vertex 鄰接表中,該邊的弧尾頂點的在頂點陣列中的下標。
     * @param next 鄰接表中,該邊的弧尾頂點的下一條出邊。 
     */
    constructor(vertex: number, next: Edge) {
        this.Vertex = vertex;
        this.Next = next;
    }
}

/**
 * 對圖G進行拓撲排序。圖G中若無環,則可進行拓撲排序,否則會輸出錯誤資訊。
 * @param graph 圖G。
 */
function topologicalSort(graph: Vertex[]) {
    let result: string[] = [];
    // 1.用於判斷最終圖中是否還有入度為0的頂點。若沒有則拓撲排序成功。否則沒有。
    let counter: number = 0;
    // 快取入度為0的頂點。
    let s: Vertex[] = [];

    // 2.將所有入度為0的頂點入棧。
    for (let i = 0; i < graph.length; i++) {
        let v: Vertex = graph[i];

        if (v.InDegree === 0) {
            s.push(v);
        }
    }

    // 3.若棧S不為空,
    while (s.length != 0) {
        // 則從棧中彈出一個頂點Vtop。
        let top: Vertex = s.pop();

        // 4.將頂點Vtop的出邊弧頭
        for (let e: Edge = top.Edge; e !== null; e = e.Next) {
            // 弧頭頂點在頂點陣列中的索引。
            let n: number = e.Vertex;

            let adj: Vertex = graph[n];

            // 頂點Vadj的入度減1。
            adj.InDegree--;

            // 5.如果Vadj的入度為0,
            if (adj.InDegree == 0) {
                // 則將Vadj入棧S。
                s.push(adj);
            }
        }
        result.push(top.Data);

        // 6.計數器遞增。
        counter++;

        // 7.跳至步驟3。
    }

    if (counter != graph.length) {
        console.log(`錯誤:圖G中頂點數:${graph.length},還剩${graph.length - counter}個頂點入度非0。`);
    }
    else {
        console.log(result.join(","));
    }
}

/**
 * 對圖G進行拓撲排序。圖G中若無環,則可進行拓撲排序,否則會輸出錯誤資訊。
 * @param graph 圖G。
 */
function topologicalSortWithDebug(graph: Vertex[]) {
    let result: string[] = [];
    // 1.用於判斷最終圖中是否還有入度為0的頂點。若沒有則拓撲排序成功。否則沒有。
    let counter: number = 0;
    // 快取入度為0的頂點。
    let s: Vertex[] = [];

    // 2.將所有入度為0的頂點入棧。
    for (let i = 0; i < graph.length; i++) {
        let v: Vertex = graph[i];

        if (v.InDegree === 0) {
            s.push(v);
        }
    }

    console.log(`初始所有入度為0的頂點:${PrintStackVertex(s)}`);

    // 3.若棧S不為空,
    while (s.length != 0) {
        console.log(`第${counter}次迴圈開始時棧:${PrintStackVertex(s)}`);
        // 則從棧中彈出一個頂點Vtop。
        let top: Vertex = s.pop();
        console.log(`${top.Data}出棧:${PrintStackVertex(s)}`);

        // 4.將頂點Vtop的出邊弧頭
        for (let e: Edge = top.Edge; e !== null; e = e.Next) {
            // 弧頭頂點在頂點陣列中的索引。
            let n: number = e.Vertex;

            let adj: Vertex = graph[n];

            // 頂點Vadj的入度減1。
            //adj.InDegree--;
            console.log(`${adj.Data}入度:${adj.InDegree}-1=${--adj.InDegree}。`);

            // 5.如果Vadj的入度為0,
            if (adj.InDegree == 0) {
                // 則將Vadj入棧S。
                s.push(adj);
                console.log(`${adj.Data}入棧。棧:${PrintStackVertex(s)}`);
            }
        }
        result.push(top.Data);
        console.log(`第${counter}次迴圈結束時棧:${PrintStackVertex(s)}`);

        // 6.計數器遞增。
        counter++;

        // 7.跳至步驟3。
    }

    if (counter != graph.length) {
        console.log(`錯誤:圖G中頂點數:${graph.length},還剩${graph.length - counter}個頂點入度非0。`);
    }
    else {
        console.log(result.join(","));
    }
}

function PrintStackVertex(s: Vertex[]): string {
    let result: string[] = [];
    
    s.forEach(e=>result.push(e.Data));

    return `棧頂->[${result.join(",")}]`;
}

function main() {
    // let vs: Vertex[] = [
    //     new Vertex(0, "0", [11, 5, 4]),
    //     new Vertex(0, "1", [8, 4, 2]),
    //     new Vertex(2, "2", [9, 6, 5]),
    //     new Vertex(0, "3", [13, 2]),
    //     new Vertex(2, "4", [7]),
    //     new Vertex(3, "5", [12, 8]),
    //     new Vertex(1, "6", [5]),
    //     new Vertex(2, "7", []),
    //     new Vertex(2, "8", [7]),
    //     new Vertex(1, "9", [11, 10]),
    //     new Vertex(1, "10", [13]),
    //     new Vertex(2, "11", []),
    //     new Vertex(1, "12", [9]),
    //     new Vertex(2, "13", [])
    // ];

    // topologicalSort(vs);

    let vertexes: Vertex[] = [
        new Vertex(0, "開始做菜", [1, 2, 3]),
        //new Vertex(2, "準備蒜苗", new int[]{4, 1}), // 讓圖中存在環
        new Vertex(1, "準備蒜苗", [4]),
        new Vertex(1, "準備辣椒", [4]),
        new Vertex(1, "準備豬肉", [5]),
        new Vertex(2, "浸泡蔬菜", [6]),
        new Vertex(1, "炒肉", [6]),
        new Vertex(2, "放入蔬菜", [7]),
        new Vertex(1, "完成", [])
    ];
    //topologicalSort(vertexes);
    topologicalSortWithDebug(vertexes);
}

main();

參考資料

《大話資料結構》 - 程傑 著 - 清華大學出版社 第270頁

相關文章