數據結構知識整理 - 應用圖(一)- 最小生成樹與最短路徑

主要內容

普利姆算法(加點法)

克魯斯卡爾算法(加邊法)

迪傑斯特拉算法(單源點)

弗洛伊德算法(頂點對)


 

現實生活中的許多問題都可以轉化爲圖來解決。例如,如何以最小成本構建一個通信網絡,如何計算地圖中兩地之間的最短路徑,如何爲複雜活動中各子任務的完成尋找一個較優的順序等。

下面將介紹圖的四個常用算法:最小生成樹最短路徑拓撲排序關鍵路徑


 

最小生成樹

假設要在n個城市之間建立通信聯絡網,則連通n個城市只需要n-1條線路。這時,自然會考慮如何在最省經費的前提下完成任務。

在一個連通網的所有生成樹中,各邊的代價之和最小的那棵生成樹稱爲該連通網的最小生成樹

MST性質:最小生成樹中必定存在一條具有最小權值的邊。普利姆(Prim)算法克魯斯卡爾(Kruskal)算法是兩個利用MST性質構成最小生成樹的算法。

普利姆算法的核心思想是歸併點,時間複雜度爲O(n²),適用於稠密網;

克魯斯卡爾算法的核心思想是歸併邊,時間複雜度爲O(elog2e),使用與稀疏網。

 

 

普利姆算法(加點法)

<邏輯思路>

(1)設所有頂點保存在集合V中,已被歸併的點保存在集合U中,則未被歸併的點保存在集合V-U中;

(2)在圖中任意找一個起始頂點v1,v1歸入U,離開V-U;

(3)頂點v1存在v2,v3,v4三個鄰接頂點,找出權值最小的邊(v1,v3);

(4)頂點v3歸入U,離開V-U;

(5)頂點v1剩餘鄰接頂點v2,v4,頂點v3有鄰接頂點v2,v4,v5,v6;

(6)比較(v1,v2)和(v3,v2),得出權值更小邊(v3,v2);比較(v1,v4)和(v3,v4),兩邊權值相同;

*在邏輯思路中,其實第(6)步可以省略,直接比較所有邊的權值,再從中選擇權值最小的邊。但代碼實現中,應該避免數據冗餘,先篩選部分意義重合的數據。

(7)比較(v3,v2),(v1,v4)或(v3,v4),(v3,v5),(v3,v6),找出權值最小的邊(v3,v6);

(8)頂點v6歸入U,離開V-U;

(9)頂點v1剩餘鄰接頂點v4(因爲(v3,v2)的權值更小,所以不再需要考慮v1到v2的情況),頂點v3剩餘鄰接頂點v2,v4,v5,頂點v6有鄰接頂點v4,v5;

(10)到這裏思路應該清晰了。

 

<實現思路>——以鄰接矩陣爲存儲結構的無向網

(1)頂點集合爲V等價於鄰接矩陣圖中用於存儲頂點信息的一維數組vexs[vexnum]

(2)算法最巧妙的地方——結構體數組closedge[vexnum],包含信息:最小邊在集合U中的那個頂點(adjvex)和最小邊的權值(lowcost)。

結構體數組closedge[]的使用正是<邏輯思路>中步驟(6)的體現。

closedge[vi-1]表示頂點vi,當lowcast不爲0時,vi在集合V-U中;當lowcast記爲0時,vi歸併到集合U中

(3)循環執行某一段代碼,直至closedge[]中所有元素的lowcast屬性都歸0,即所有頂點都併入到集合U中。

 

(看代碼前可以先回顧下“鄰接矩陣”的知識)

typedef struct                        /*定義結構體數組closedge[vexnum]*/
{
    Vextype adjvex;
    Arctype lowcast;
} closedge[vexnum];
    

void MiniSpanTree_Prim(AMGraph G, Vextype vi)
{
    int i = LocateVex(G, vi);         /*確定起始頂點vi的編號*/

    closedge[i] = {NULL, 0};          /*將vi歸併到集合U中*/
    
    for(int vj = 1; vj <= G.vexnum; vj++)    /*對於V-U中的每個頂點vj,初始化closedge[vj-1]*/
    {
        int j = LocateVex(G, vj);

        if(j != i) closedge[j] = {vi, G.arcs[i][j]};
    }

    for(int k = 1; k < G.vexnum; k ++)       /*直到所有頂點歸併到集合U前,循環執行某一段代碼*/
    {
        i = Min(closedge);            /*函數Min()找出closedge[]中lowcast最小的元素,並返回下標i*/
                                      /*即找出權值最小的邊,並找出位於V-U中的頂點vj*/
    closedge[i].lowcast = 0;          /*將頂點vj歸併到集合U中*/

    u0 = closedge[i].adjvex           /*u0爲最小邊在U中的點*/
    v0 = G.vexs[i];                   /*v0爲最小邊在V-U中的點*/
    cout<<u0<<v0;                     /*輸出u0,v0以記錄路線*/

    for(int j = 0; j < G.vexnum; j++)
    {
        /*之前對closedge[]的元素進行過初始化,併入新的點後要重新選出權值更小的邊,對應<邏輯思路>的步驟(6)*/
        if(G.arcs[i][j] < closedge[j].lowcast)    
            closedge[j] = {G.vexs[i], G.arcs[i][j]};  
    }
}

 

 

克魯斯卡爾算法(加邊法)

如果說普利姆算法是“加點法”,那麼克魯斯卡爾算法就是“加邊法”。

<邏輯思路>

(1)將由n個頂點組成的連通圖拆分成n個連通分量,即每個頂點爲一個連通分量;

(2)將圖上的所有邊按權值排序;

(3)從最小邊開始操作,歸併邊判斷條件是下一條被選邊不能使連通分量形成迴路,即被選邊的兩個頂點head和tail不能在同一個連通分量上

(4)在循環中執行某段代碼,直至所有頂點被歸併到同一連通分量。

 

<實現思路>——以鄰接矩陣爲存儲結構的無向網

(1)按權值排序可以使用“冒泡法”“選擇法”

(2)從步驟(3)可以看出,我們需要類似於普利姆算法中的closedge[]那樣的輔助數組。包含的信息:每一條邊的頭頂點和尾頂點以及邊上的權值;

(3)同時,我們還需要一個標記數組輔助判斷每一個頂點所屬的連通分量

(4)歸併一個頂點後,將頂點的連通分量改爲併入它的連通分量。

 

typedef struct                         /*結構體數組的各元素代表各邊*/
{
    Vextype head;
    Vextype tail;
    Arctype lowcast;
} Arcs[arcnum];

int VexSet[vexnum];                    /*VexSet[vi-1] = i;表示vi所在的連通分量編號爲i,即它本身*/

void MiniSpanTree_Kruskal(AMGraph G)
{
    for(int t = 0; t < arcnum; t++)    /*輸入各邊信息*/
        cin>>Arcs[t].head>>Arcs[t].tail>>Arcs[t].lowcast;

    Sort(Arcs);                        /*按權值將圖上各邊從小到大排序*/

    for(int t = 0; t < arcnum; t++)    /*對圖上所有邊(權值從小到大)依次進行操作*/
    {
        Headv = LocateVex(G, Arcs[t].head);        /*確定頭尾頂點的編號*/
        Tailv = LocateVex(G, Arcs[t].tail);
        
        VS_h = VexSet[HeadV];                      /*確定頭尾頂點所在的連通分量*/
        VS_t = VexSet[Tailv];

        if(VS_h != VS_t)                           /*若兩個頂點不在同一連通分量*/
        {
            cout<<Arcs[t].head<<Arcs[t].tail;      /*輸出符合要求的邊*/

            for(int v = 0; v < G.vexnum; v++)      /*對所有頂點進行判斷*/
                if(VexSet[v] == VS_t)
                    VexSet[v] = VS_H;              /*將頂點歸併到同一連通分量*/
        }
    }
}
    

 

最短路徑

最簡單的最短路徑是求中轉次數最少的路徑,而不考慮每條邊的權值。而在實際問題中,路徑長度的度量就不再是路徑上的邊數,而是路徑上所有邊的權值之和

有向網中,習慣上稱路徑的第一個頂點爲源點(Source),最後一個頂點爲終點(Destination)

下面主要討論兩種最常見的最短路徑問題:

一、從某個源點到其餘頂點的最短路徑;

二、求每一對頂點之間的最短路徑。

迪傑斯特拉(Dijkstra)算法用於求解第一種問題,時間複雜度爲O(n²)弗洛伊德(Floyd)算法用於求解第二種問題,時間複雜度爲O(n³)

從實現形式上來說,弗洛伊德算法比迪傑斯特拉算法更爲簡潔

 

 

迪傑斯特拉算法(單源點)

<邏輯思路>

(1)從單源點出發,求到各個頂點的最短路徑。該問題類似於就有向連通網最小生成樹

(2)因此同樣需要輔助數組來記錄頂點是否被歸併最短路徑長度

(3)迪傑斯特拉算法的巧妙之處就在於設計了三個輔助數組:

1. 一維數組S[]:記錄頂點vi是否被確定最短路徑長度,即該頂點是否被歸併到終點集合中。初始化:S[v0-1] = TRUE;

2. 一維數組Path[]:記錄頂點vi的直接前驅。譬如存在最短路徑<v1,v4,v3>,v4的直接前驅是v1,v3的直接前驅是v4,這樣一來,依靠直接前驅數組就能將路徑連接起來。初始化:若源點v0到vi有弧,則Path[vi-1]爲v0-1(編號/下標);否則爲-1。

3. 一維數組D[]:記錄從源點v0到終點vi的最短路徑長度初始化:若源點v0到vi有弧,則D[vi-1]爲弧上的權值;否則爲∞。

(4)算法開始時,先找到D[]上的最小值,然後併入第一個終點v1,並將S[v1-1]的值設爲TRUE;

(5)由於加入了終點v1,相當於v0多了一個中轉點,所以需要對D[]上的值進行更新;

(6)更新後繼續找D[]上的最小值,繼而找到了頂點v2,因爲S[v2-1]的值爲FALSE,即v2未被歸併到終點集合,所以可以將它併入;

(7)若到v2的最短路徑是<v0,v1,v2>,則將v2的直接前驅改爲v1,即Path[v2-1] = v1-1

(8)反覆執行以上過程(n-1次),直至所有頂點被併入終點集合

 

<實現思路>——以鄰接矩陣爲存儲結構的有向網

(1)對應<邏輯思路>步驟(3)的初始化:

1. S[v0-1] = TRUE;

2. D[vi-1] = G.arcs[v0-1][vi-1];

3. if(G.arcs[v0-1][vi-1] != MaxInt) Path[vi-1] = v0-1;              /*MaxInt表示無窮∞,值爲326767*/

(2)對應<邏輯思路>步驟(8)的n-1次循環

1. 步驟(4):D[v1]  = Min(D);

2. 步驟(6):if(S[v1-1] = FALSE) S[v1-1] = TRUE;

3. 步驟(5)和步驟(7): if(D[v1-1] + G.arcs[v1-1][v2-1] < D[v2-1])  {D[v2-1] = D[v1-1] + G.arcs[v1-1][v2-1]; Path[v2-1] = v1-1;}

 

#define TRUE 1
#define FALSE 0

void ShortestPath_DIJ(AMGraph G, int v0)
{
    int S[vexnum];
    int Path[vexnum];
    int D[vexnum];

    int i = LocateVex(G, v0);

    for(int vi = 1; vi <= G.vexnum; vi++) /*初始化*/
    {
        int j  =LocateVex(G, vi);

        S[j] = FALSE;

        D[j] = G.arcs[i][j];

        if(D[j] < MaxInt) Path[j] = i;    /*小於MaxInt即v0與vi間存在弧*/
        else Path[j] = -1;                /*若vi的直接前驅不是v0,則置爲-1*/
    }
    
    S[i] = TRUE;
    D[i] = 0;                             /*源點到源點的路徑長度爲0*/

/*----------初始化結束-----------*/

    for(int t = 1; t < G.vexnum; t++)     /*循環n-1次*/
    {
        int v;                            /*下一個終點*/

        for(vi = 1; vi <= G.vexnum; vi++)
        {
            int min = MaxInt;             /*min保存最小值*/

            int j = LocateVex(G, vi);

            if(S[j] = FALSE && D[j] < min)/*找出最小權值的邊*/
                {v = vi; min = D[j];}
        }

        S[v-1] = TRUE;                    /*將v加入終點集合*/
        
        int k = LocateVex(G, v);
        
        for(vi = 1; vi <= G.vexnum; vi++) /*更新最短路徑和直接前驅數組*/
        {
            int j = LocateVex(G, vi);

            if(S[j] = FALSE && (D[k] + G.arcs[k][j] < D[j])
            {
                D[j] = D[k] + G.arcs[k][j];
                Path[j] = k;
            }
        }
    }
}
                

 

弗洛伊德算法(頂點對)

求解每一對頂點之間的最短路徑有兩種方法:

一種是n次調用迪傑斯特拉算法;

另一種是採用下面介紹的弗洛伊德算法。

兩種算法的時間複雜度均爲O(n³),但弗洛伊德算法的形式更爲簡潔。

 

<邏輯思路>

(1)如果說迪傑斯特拉算法是一個“加邊”的過程,那麼弗洛伊德算法就是一個“加點”的過程;

(2)先確定兩個目標頂點,源點爲vs,終點爲ve;

(3)若vs和ve之間存在弧,初始化爲arcs[vs-1][ve-1](編號),反之置爲∞;

(4)依次判斷剩餘頂點vi,若在兩個頂點間插入vi後,使得<vs,vi>+<vi,ve>小於<vs,ve>,則置換爲vs和ve間的最短路徑;

(5)直至所有剩餘頂點判斷完畢,最短路徑確定;

(6)其餘的每一對頂點都重複上述過程。

 

<實現思路>——以鄰接矩陣爲存儲結構的有向網

(1)該算法的核心思想是設置了兩個二維輔助數組:

1. 二維數組Path[][]:行表示頂點vi的直接前驅,列表示頂點vi;

2. 二維數組D[]][]:記錄vs和ve間的最短路徑。

(2)弗洛伊德算法的代碼思路比較清晰,插入、比較、更新。

 

void ShortestPath_Floyd(AMGraph G)
{
/*-----------初始化------------*/
    for(int vs = 1; vs <= G.vexnum; vs++)         /*源點vs*/
    {
        int i = LocateVex(G, vs);

        for(int ve = 1; ve <= G.vexnum; ve++)     /*終點ve*/
        {
            int j = LocateVex(G, ve);

            D[i][j] = G.arcs[i][j];
            
            if(D[i][j] < MaxInt && i != j)
                Path[i][j] = i;
            else Path[i][j] = -1;
        }
           
/*---------初始化結束--------*/

    for(int vi = 1; vi <= G.vexnum; vi++)         /*插入點vi*/
        for(int vs = 1; vs <= G.vexnum; vs++)     /*源點vs*/
            for(int ve = 1; ve <= G.vexnum; ve++) /*終點ve*/
                {
                    int k = LocateVex(G, vi);
                    int i = LocateVex(G, vs);
                    int j = LocateVex(G, ve);
            
                    if(D[i][k] + D[k][j] < D[i][j])       /*更新*/
                    {
                        D[i][j] = D[i][k] + D[k][j];
                        
                        Path[i][j] = k;
                    }
                }
}

 

路過的圈毛君:“‘拓撲排序’和‘關鍵路徑’的內容可能要先放一放,以後一定會補上的!_(:з」∠)_”

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章