數據結構(五)圖---最小生成樹(普里姆算法)


一:最小生成樹

(一)定義

我們把構造連通網的最小代價生成樹稱為最小生成樹

給定一個帶權的無向連通圖,如何選取一棵生成樹,使樹上所有邊上權的總和為最小,這叫最小生成樹.

(二)什么是最小生成樹?

1.是一棵樹

1)無回路
2)N個頂點,一定有N-1條邊

2.是生成樹

1)包含全部頂點
2)N-1條邊都在圖中

3.邊的權重和最小

(三)案例說明

在實際生活中,我們常常碰到類似這種一類問題:如果要在n個城市之間建立通信聯絡網,
則連通n個城市僅僅須要n
-1條線路。這時。我們須要考慮這樣一個問題。怎樣在最節省經費前提 下建立這個通信網.換句話說,我們須要在這n個城市中找出一個包括全部城市的連通子圖,使得 其全部邊的經費之和最小. 這個問題能夠轉換為一個圖論的問題:圖中的每一個節點看成是一個城市, 節點之間的無向邊表示修建該路的經費。即每條邊都有其對應的權值,而我們的目標是挑選n-1條 邊使全部節點保持連通。而且要使得經費之和最小. 這里存在一個顯而易見的事實是: 最優解中必定不存在循環(可通過反證法證明). 因此。最后找 出的包括全部城市的連通子圖必定沒有環路。 這樣的連通且沒有環路的連通圖就簡稱為。而在一個 連通圖中刪除全部的環路而形成的樹叫做該圖的生成樹.對於城市建立通信連通網。須要找出的樹由 於具有最小的經費之和。因此又被稱為最小生成樹(Minimum Cost Spanning Tree),簡稱MST.

(四)求最小生成樹的算法

(1) 普里姆算法

圖的存貯結構采用鄰接矩陣.此方法是按各個頂點連通的步驟進行,需要用一個頂點集合,開始為空集,以后將以連通的頂點陸續加入到集合中,全部頂點加入集合后就得到所需的最小生成樹 .

(2) 克魯斯卡爾算法

圖的存貯結構采用邊集數組,且權值相等的邊在數組中排列次序可以是任意的.該方法對於邊相對比較多的不是很實用,浪費時間.

二:貪心算法

1.什么是貪?

每一步都要最好(只看下一步)

2.什么是好?

權重最小的邊

3.需要約束

1.只能用圖里有的邊
2.只能正好用掉N-1條邊
3.不能有回路

三:普里姆算法(稠密圖)

(一)定義

對於一個帶權的無向連通圖,其每個生成樹所有邊上的權值之和可能不同,我們把所有邊上權值之和最小的生成樹稱為圖的最小生成樹。
普里姆算法是以其中某一頂點為起點,逐步尋找各個頂點上最小權值的邊來構建最小生成樹。
其中運用到了回溯,貪心的思想。

 (二)算法思路

設圖G=(V,E),U是頂點集V的一個非空子集。假設(u,v)是一條具有最小權值的邊。當中u∈U,v∈V-U,

則必存在一棵包括邊(u,v)的最小生成樹.

上述的性質能夠通過反證法證明。假設(u,v)不包括在G的最小生成樹T中。那么,T的路徑中必定存

在一條連通U和V-U的邊,假設將這條邊以(u,v)來替換,我們將獲得一個權重更低的生成樹,這與T

是最小生成樹矛盾.既然MST滿足貪婪選擇屬性。那么。求解最小生成樹的問題就簡化了非常多。

總結一下,詳細的步驟大概例如以下:

1.構建一棵空的最小生成樹T。並將全部節點賦值為無窮大.
2.任選一個節點放入T。另外一個節點集合為V-T.
3.對V-T中節點的賦值進行更新(因為此時新增加一個節點,這些距離可能發生變化)
4.從V-T中選擇賦值最小的節點,增加T中
5.假設V-T非空,繼續步驟3~5,否則算法終結

(三)步驟模擬

原圖:

以上圖G4為例,來對普里姆進行演示(從第一個頂點A開始通過普里姆算法生成最小生成樹)。

初始狀態:V是所有頂點的集合,即V={A,B,C,D,E,F,G};U和T都是空! 

第1步:將頂點A加入到U中。 
    此時,U={A}。 
第2步:將頂點B加入到U中。 
    上一步操作之后,U={A}, V-U={B,C,D,E,F,G};因此,邊(A,B)的權值最小。將頂點B添加到U中;此時,U={A,B}。 
第3步:將頂點F加入到U中。 
    上一步操作之后,U={A,B}, V-U={C,D,E,F,G};因此,邊(B,F)的權值最小。將頂點F添加到U中;此時,U={A,B,F}。 
第4步:將頂點E加入到U中。 
    上一步操作之后,U={A,B,F}, V-U={C,D,E,G};因此,邊(F,E)的權值最小。將頂點E添加到U中;此時,U={A,B,F,E}。 
第5步:將頂點D加入到U中。 
    上一步操作之后,U={A,B,F,E}, V-U={C,D,G};因此,邊(E,D)的權值最小。將頂點D添加到U中;此時,U={A,B,F,E,D}。 
第6步:將頂點C加入到U中。 
    上一步操作之后,U={A,B,F,E,D}, V-U={C,G};因此,邊(D,C)的權值最小。將頂點C添加到U中;此時,U={A,B,F,E,D,C}。 
第7步:將頂點G加入到U中。 
    上一步操作之后,U={A,B,F,E,D,C}, V-U={G};因此,邊(E,G)的權值最小。將頂點G添加到U中;此時,U=V。

此時,最小生成樹構造完成!它包括的頂點依次是:A B F E D C G

(三)算法實現

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>

#define MAXVEX 100    //最大頂點數
#define INFINITY 65535    //用0表示∞

typedef char VertexType;    //頂點類型,字符型A,B,C,D...
typedef int EdgeType;    //邊上權值類型10,15,...

//鄰接矩陣結構
typedef struct
{
    VertexType vers[MAXVEX];    //頂點表
    EdgeType arc[MAXVEX][MAXVEX];    //鄰接矩陣,可看作邊表
    int numVertexes, numEdges;    //圖中當前的頂點數和邊數
}MGraph;


void CreateMGraph(MGraph* G);
void showGraph(MGraph G);

void MiniSpanTree_prim(MGraph G);    //Prim算法生成最小生成樹

//Prim算法生成最小生成樹
void MiniSpanTree_prim(MGraph G)
{
    int min, i, j, k;
    int adjvex[MAXVEX];        //保存相關頂點下標
    int lowcost[MAXVEX];    //保存相關頂點間邊的權值
    lowcost[0] = 0;    //初始化第一個權值為0,即將v0加入生成樹
    //lowcost的值為0表示此下標的頂點已經加入生成樹
    adjvex[0] = 0;    //初始化第一個頂點下標為0
    for (i = 1; i < G.numVertexes;i++)
    {
        lowcost[i] = G.arc[0][i];    //將v0頂點與之有關的邊的權值都存放入權值數組
        adjvex[i] = 0;    //初始化都為v0的下標
    }

    for (i = 1; i < G.numVertexes;i++)
    {
        min = INFINITY;
        j = 1;
        k = 0;
        while (j<G.numVertexes)
        {
            if (lowcost[j]!=0&&lowcost[j]<min)
            {
                //如果權值不為0且權值小於min
                min = lowcost[j];    //則讓當前權值成為最小值
                k = j;    //將當前最小值的下標存放k
            }
            j++;
        }
        printf("(%d,%d)", adjvex[k], k);    //打印當前頂點邊中權值最小邊
        lowcost[k] = 0;//將當前頂點的權值置為0,表示此頂點已經完成任務

        //和上面做了幾乎一樣的操作,就是更新權值
        for (j = 1; j < G.numVertexes;j++)    //循環所有頂點,因為我們已經確認第一個放入的0,所有我們循環可以省去0
        {
            if (lowcost[j] != 0 && G.arc[k][j]<lowcost[j])    
            {//若下標為k頂點個邊權值小於此前頂點,就不會加入生成樹權值
                lowcost[j] = G.arc[k][j];    //將較小權值存入lowcost
                adjvex[j] = k;        //將下標為k的頂點存入adjvex
            }
        }
    }
}

int main()
{
    MGraph MG;
    CreateMGraph(&MG);
    showGraph(MG);
    MiniSpanTree_prim(MG);
    system("pause");
    return 0;
}

void CreateMGraph(MGraph* G)
{
    int i, j, k, w;
    G->numVertexes = 9;
    G->numEdges = 15;
    //讀入頂點信息
    G->vers[0] = 'A';
    G->vers[1] = 'B';
    G->vers[2] = 'C';
    G->vers[3] = 'D';
    G->vers[4] = 'E';
    G->vers[5] = 'F';
    G->vers[6] = 'G';
    G->vers[7] = 'H';
    G->vers[8] = 'I';

    //getchar();    //可以獲取回車符
    for (i = 0; i < G->numVertexes; i++)
        for (j = 0; j < G->numVertexes; j++)
            G->arc[i][j] = INFINITY;    //鄰接矩陣初始化

    G->arc[0][1] = 10;
    G->arc[0][5] = 11;
    G->arc[1][2] = 18;
    G->arc[1][6] = 16;
    G->arc[1][8] = 12;
    G->arc[2][3] = 22;
    G->arc[2][8] = 8;
    G->arc[3][4] = 20;
    G->arc[3][7] = 16;
    G->arc[3][6] = 24;
    G->arc[3][8] = 21;
    G->arc[4][5] = 26;
    G->arc[4][7] = 7;
    G->arc[5][6] = 17;
    G->arc[6][7] = 19;

    for (k = 0; k < G->numVertexes; k++)    //讀入numEdges條邊,建立鄰接矩陣
    {
        for (i = k; i < G->numVertexes; i++)
        {
            G->arc[i][k] = G->arc[k][i];    //因為是無向圖,所有是對稱矩陣
        }
    }
}



void showGraph(MGraph G)
{
    for (int i = 0; i < G.numVertexes; i++)
    {
        for (int j = 0; j < G.numVertexes; j++)
        {
            if (G.arc[i][j] != INFINITY)
                printf("%5d", G.arc[i][j]);
            else
                printf("    0");
        }
        printf("\n");
    }
}

(四)普里姆代碼分析

//Prim算法生成最小生成樹
void MiniSpanTree_prim(MGraph G)
{
    int min, i, j, k;
    int adjvex[MAXVEX];        //保存相關頂點下標
    int lowcost[MAXVEX];    //保存相關頂點間邊的權值
    lowcost[0] = 0;    //初始化第一個權值為0,即將v0加入生成樹
    //lowcost的值為0表示此下標的頂點已經加入生成樹
    adjvex[0] = 0;    //初始化第一個頂點下標為0
    for (i = 1; i < G.numVertexes;i++)
    {
        lowcost[i] = G.arc[0][i];    //將v0頂點與之有關的邊的權值都存放入權值數組
        adjvex[i] = 0;    //初始化都為v0的下標
    }

    for (i = 1; i < G.numVertexes;i++)
    {
        min = INFINITY;
        j = 1;
        k = 0;
        while (j<G.numVertexes)
        {
            if (lowcost[j]!=0&&lowcost[j]<min)
            {
                //如果權值不為0且權值小於min
                min = lowcost[j];    //則讓當前權值成為最小值
                k = j;    //將當前最小值的下標存放k
            }
            j++;
        }
        printf("(%d,%d)", adjvex[k], k);    //打印當前頂點邊中權值最小邊
        lowcost[k] = 0;//將當前頂點的權值置為0,表示此頂點已經完成任務

        //和上面做了幾乎一樣的操作,就是更新權值
        for (j = 1; j < G.numVertexes;j++)    //循環所有頂點,因為我們已經確認第一個放入的0,所有我們循環可以省去0
        {
            if (lowcost[j] != 0 && G.arc[k][j]<lowcost[j])    
            {//若下標為k頂點個邊權值小於此前頂點,就不會加入生成樹權值
                lowcost[j] = G.arc[k][j];    //將較小權值存入lowcost
                adjvex[j] = k;        //將下標為k的頂點存入adjvex
            }
        }
    }
}

下面是我們要處理的鄰接矩陣

1.創建兩個數組,一個存放頂點,一個存放相關頂點間邊的權值

int adjvex[MAXVEX];        //保存相關頂點下標
int lowcost[MAXVEX];    //保存相關頂點間邊的權值

作用:

adjvex數組:將存放我們左側的頂點下標 lowcost數組:將存放我們對應頂點的各個邊的權值

會利用這兩個來打印出我們所需要的最小邊

printf("(%d,%d)", adjvex[k], k); //打印當前頂點邊中權值最小邊   
其中adjvex[k]存放的是我們左側的弧尾,k是我們找的的鄰接點權值最小的弧頭

注意:

比如我們要找v3頂點作為弧頭的邊,那么我們adjvex[3]中將會存放其弧尾,也就是我們的左側下標
那么我們去找第一條邊時,我們只知道與v0相鄰頂點間邊的權值,並不知道k值,所以我們開始無法知道adjvex[k]=0中k是誰。
但是我們可以在開始對頂點數組adjvex進行初始化,全部初始化為0,就可以解決這個問題
    lowcost[0] = 0;    //初始化第一個權值為0,即將v0加入生成樹
    //lowcost的值為0表示此下標的頂點已經加入生成樹
    adjvex[0] = 0;    //初始化第一個頂點下標為0
    for (i = 1; i < G.numVertexes;i++)
    {
        lowcost[i] = G.arc[0][i];    //將v0頂點與之有關的邊的權值都存放入權值數組
        adjvex[i] = 0;    //初始化都為v0的下標
    }
其中lowcost[0] = 0;是因為我們開始就將v0點放入生成樹中,所以要將對應的lowcost[0]設置為0,我們會在后面,將所有的放入生成樹中的頂點全部設置為0,但是注意生成樹在代碼中不是直接出現的
其中lowcost[i] = G.arc[0][i];  是將對應的鄰接頂點的權值放入lowcost中

2.循環所有的左側頂點,獲取他們相關的最小鄰接邊

    for (i = 1; i < G.numVertexes;i++)
    {
        min = INFINITY;
        j = 1;
        k = 0;
        while (j<G.numVertexes)
        {
            if (lowcost[j]!=0&&lowcost[j]<min)
            {
                //如果權值不為0且權值小於min
                min = lowcost[j];    //則讓當前權值成為最小值
                k = j;    //將當前最小值的下標存放k
            }
            j++;
        }
        printf("(%d,%d)", adjvex[k], k);    //打印當前頂點邊中權值最小邊
        lowcost[k] = 0;//將當前頂點的權值置為0,表示此頂點已經完成任務

        //和上面做了幾乎一樣的操作,就是更新權值
        for (j = 1; j < G.numVertexes;j++)    //循環所有頂點,因為我們已經確認第一個放入的0,所有我們循環可以省去0
        {
            if (lowcost[j] != 0 && G.arc[k][j]<lowcost[j])    
            {//若下標為k頂點個邊權值小於此前頂點,就不會加入生成樹權值
                lowcost[j] = G.arc[k][j];    //將較小權值存入lowcost
                adjvex[j] = k;        //將下標為k的頂點存入adjvex
            }
        }
    }

其中while循環是獲取我們的權值中最小的那個的弧頭下標,將會和弧尾組成一條邊:

        while (j<G.numVertexes)
        {
            if (lowcost[j]!=0&&lowcost[j]<min)
            {
                //如果權值不為0且權值小於min
                min = lowcost[j];    //則讓當前權值成為最小值
                k = j;    //將當前最小值的下標存放k
            }
            j++;
        }

下面的權值都會存在lowcost中

           

    printf("(%d,%d)", adjvex[k], k);  //可以打印處這條邊

我們將找到的這個頂點和上面初始時設置的lowcost一樣設置為0,表示已經加入生成樹,我們不必去修改他們

    lowcost[k] = 0;//將當前頂點的權值置為0,表示此頂點已經完成任務

下面的for循環和我們之前的for循環更新權值是一致的,但是有些不同

        //和上面做了幾乎一樣的操作,就是更新權值
        for (j = 1; j < G.numVertexes;j++)    //循環所有頂點,因為我們已經確認第一個放入的0,所有我們循環可以省去0
        {
            if (lowcost[j] != 0 && G.arc[k][j]<lowcost[j])    
            {//若下標為k頂點個邊權值小於此前頂點,就不會加入生成樹權值
                lowcost[j] = G.arc[k][j];    //將較小權值存入lowcost
                adjvex[j] = k;        //將下標為k的頂點存入adjvex
            }
        }

首先我們做了比較

if (lowcost[j] != 0 && G.arc[k][j]<lowcost[j])   
首先頂點不能及時生成樹中的,即lowcost[j]!=0,然后其弧權值需要比原來的權值小才行,因為可能出現原來的權值更加小,這是就要選擇原來的邊作為新的路徑

                lowcost[j] = G.arc[k][j];    //將較小權值存入lowcost
                adjvex[j] = k;        //將下標為k的頂點存入adjvex
我們更新了權值最新值,會在下一次的循環中再次選取下一個點

四:總結

從指定頂點開始將它加入集合中,然后將集合內的頂點與集合外的頂點所構成的所有邊中選取權值最小的一條邊作為生成樹的邊,並將集合外的那個頂點加入到集合中,表示該頂點已連通.
再用集合內的頂點與集合外的頂點構成的邊中找最小的邊,並將相應的頂點加入集合中,如此下去直到全部頂點都加入到集合中,即得最小生成樹.
普利姆算法適合稠密圖,其時間復雜度為O(n^2),其時間復雜度與邊的數目無關

 


免責聲明!

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



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