算法:圖解最小生成樹之普里姆(Prim)算法

1)算法的基本思想:
普里姆算法的基本思想:普里姆算法是一種構造最小生成樹的算法,它是按逐個將頂點連通的方式來構造最小生成樹的。

從連通網絡N = { V,E }中的某一頂點u0出發,選擇與它關聯的具有最小權值的邊(u0, v),將其頂點加入到生成樹的頂點集合U中。以後每一步從一個頂點在U中,而另一個頂點不在U中的各條邊中選擇權值最小的邊(u,v),把該邊加入到生成樹的邊集TE中,把它的頂點加入到集合U中。如此重複執行,直到網絡中的所有頂點都加入到生成樹頂點集合U中爲止。
假設G=(V,E)是一個具有n個頂點的帶權無向連通圖,T(U,TE)是G的最小生成樹,其中U是T的頂點集,TE是T的邊集,則構造G的最小生成樹T的步驟如下:
(1)初始狀態,TE爲空,U={v0},v0∈V;
(2)在所有u∈U,v∈V-U的邊(u,v)∈E中找一條代價最小的邊(u′,v′)併入TE,同時將v′併入U;
重複執行步驟(2)n-1次,直到U=V爲止。
在普里姆算法中,爲了便於在集合U和(V-U)之間選取權值最小的邊,需要設置兩個輔助數組closest和lowcost,分別用於存放頂點的序號和邊的權值。
  對於每一個頂點v∈V-U,closest[v]爲U中距離v最近的一個鄰接點,即邊(v,closest[v])是在所有與頂點v相鄰、且其另一頂點j∈U的邊中具有最小權值的邊,其最小權值爲lowcost[v],即lowcost[v]=cost[v][closest[v]],採用鄰接表作爲存儲結構:
設置一個輔助數組closedge[]:
lowcost域存放生成樹頂點集合內頂點到生成樹外各頂點的各邊上的當前最小權值;
adjvex域記錄生成樹頂點集合外各頂點距離集合內哪個頂點最近(即權值最小)。

我們在圖的定義中說過,帶有權值的圖就是網結構。一個連通圖的生成樹是一個極小的連通子圖,它含有圖中全部的頂點,但只有足以構成一棵樹的n-1條邊。所謂的最小成本,就是n個頂點,用n-1條邊把一個連通圖連接起來,並且使得權值的和最小。綜合以上兩個概念,我們可以得出:構造連通網的最小代價生成樹,即最小生成樹(Minimum Cost Spanning Tree)。
找連通圖的最小生成樹,經典的有兩種算法,普里姆算法和克魯斯卡爾算法,這裏介紹普里姆算法。
爲了能夠講明白這個算法,我們先構造網圖的鄰接矩陣,如圖7-6-3的右圖所示。
這裏寫圖片描述
也就是說,現在我們已經有了一個存儲結構爲MGraph的MG(見《鄰接矩陣創建圖》)。MG有9個頂點,它的二維數組如右圖所示,數組中我們使用65535代表無窮。
下面我們對着程序和每一步循環的圖示來看:
算法代碼:(改編自《大話數據結構》)

/* Prim算法生成最小生成樹  */
void MiniSpanTree_Prim(MGraph MG)
{
    int min, i, j, k;
    int adjvex[MAXVEX];/* 保存相關頂點下標 */
    int lowcost[MAXVEX];/* 保存相關頂點間邊的權值 */
    lowcost[0] = 0;/* 初始化第一個權值爲0,即v0加入生成樹 */
    /* lowcost的值爲0,在這裏就是此下標的頂點已經加入生成樹 */
    adjvex[0] = 0;/* 初始化第一個頂點下標爲0 */
    cout << "最小生成樹的邊爲:" << endl;
    for (i = 1; i < MG.numVertexes; i++)
    {
        lowcost[i] = MG.arc[0][i];/* 將v0頂點與之有邊的權值存入數組 */
        adjvex[i] = 0;/* 初始化都爲v0的下標 */
    }

    for (i = 1; i < MG.numVertexes; i++)
    {
        min = INFINITY; /* 初始化最小權值爲∞, */

        j = 1;
        k = 0;

        while (j < MG.numVertexes)/* 循環全部頂點 */
        {
            if (lowcost[j] != 0 && lowcost[j] < min)/* 如果權值不爲0且權值小於min */
            {
                min = lowcost[j];/* 則讓當前權值成爲最小值 */
                k = j;/* 將當前最小值的下標存入k */
            }

            j++;
        }

        cout << "(" << adjvex[k] << ", " << k << ")" << "  "; /* 打印當前頂點邊中權值最小的邊 */
        lowcost[k] = 0;/* 將當前頂點的權值設置爲0,表示此頂點已經完成任務 */

        for (j = 1; j < MG.numVertexes; j++)/* 循環所有頂點 */
        {
            /* 如果下標爲k頂點各邊權值小於此前這些頂點未被加入生成樹權值 */
            if (lowcost[j] != 0 && MG.arc[k][j] < lowcost[j])
            {
                lowcost[j] = MG.arc[k][j];/* 將較小的權值存入lowcost相應位置 */
                adjvex[j] = k;/* 將下標爲k的頂點存入adjvex */
            }
        }
    }
    cout << endl;
}

1、程序中1~16行是初始化操作,其中第7~8行 adjvex[0] = 0 意思是現在從頂點v0開始(事實上從那一點開始都無所謂,假定從v0開始),lowcost[0]= 0 表示v0已經被納入到最小生成樹中,之後凡是lowcost數組中的值被設爲0就表示此下標的頂點被納入最小生成樹。
2、第11~15行表示讀取鄰接矩陣的第一行數據,所以 lowcost數組爲{ 0 ,10, 65535, 65535, 65535, 11, 65535, 65535, 65535 },而adjvex數組爲全0。至此初始化完畢。
3、第17~49行共循環了8次,i從1一直累加到8,整個循環過程就是構造最小生成樹的過程。
4、第24~33行,經過循環後min = 10, k = 1。注意26行的if 判斷lowcost[j] != 0 表示已經是生成樹的頂點則不參加最小權值的查找。
5、第35行,因k = 1, adjvex[1] = 0, 所以打印結果爲(0, 1),表示v0 至 v1邊爲最小生成樹的第一條邊,如下圖的第一個小圖。
6、第36行,因k = 1 將lowcost[k] = 0 就是說頂點v1納入到最小生成樹中,此時lowcost數組爲{ 0,0, 65535, 65535, 65535, 11, 65535, 65535, 65535 }
7、第38~47行,j 循環從1 到8, 因k = 1,查找鄰接矩陣的第v1行的各個權值,與lowcost數組對應值比較,若更小則修改lowcost值,並將k值存入adjvex數組中。所以最終lowcost = { 0,0, 18, 65535, 65535, 11, 16, 65535, 12 }。 adjvex數組的值爲 {0, 0, 1, 0, 0, 0, 1, 0, 1 }。這裏的if判斷也表示v0和v1已經是生成樹的頂點不參與最小權值的比對了。

上面所述爲第一次循環,對應下圖i = 1的第一個小圖,由於要用文字描述清楚整個流程比較繁瑣,下面給出i爲不同值一次循環下來後的生成樹圖示,所謂一圖值千言,大家對着圖示自己模擬地循環8次就能理解普里姆算法的思想了。
這裏寫圖片描述
即最小生成樹的邊爲:(0, 1), (0, 5), (1, 8), (8, 2), (1, 6), (6, 7), (7, 4), (7, 3)
最後再來總結一下普里姆算法的定義:
假設N = (V{E} )是連通網,TE是N上最小生成樹的集合。算法從U = { u0} ( uo V),TE = { } 開始。重複執行下述操作:在所有
uU,v V - U 的邊(u, v) E 中找一條代價最小的邊(u0 , v0) 併入集合TE, 同時v0 併入U, 直至 U = V 爲止。此時TE 中必有n-1 條邊, 則 T = (V,{TE} ) 爲N的最小生成樹。

由算法代碼中的循環嵌套可得知此算法的時間複雜度爲O(n^2)。
對比普里姆和克魯斯卡爾算法,克魯斯卡爾算法主要針對邊來展開,邊數少時效率比較高,所以對於稀疏圖有較大的優勢;而普里姆算法對於稠密圖,即邊數非常多的情況下更好一些。

轉載自http://blog.csdn.net/jnu_simba/article/details/8869876

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