最小生成树(普里姆算法【Prim】与克鲁斯卡尔算法【Kruskal】)
寫在前面:博主是一位普普通通的19屆雙非軟工在讀生,平時最大的愛好就是聽聽歌,逛逛B站。博主很喜歡的一句話花開堪折直須折,莫待無花空折枝:博主的理解是頭一次為人,就應該做自己想做的事,做自己不后悔的事,做自己以后不會留有遺憾的事,做自己覺得有意義的事,不浪費這大好的青春年華。博主寫博客目的是記錄所學到的知識并方便自己復習,在記錄知識的同時獲得部分瀏覽量,得到更多人的認可,滿足小小的成就感,同時在寫博客的途中結交更多志同道合的朋友,讓自己在技術的路上并不孤單。
目錄:
1.最小生成樹
???? ?? 生成樹概念
???? ?? 最小生成樹
2.普里姆算法【Prim】
???? ?? 普里姆算法簡介
???? ?? 普里姆算法完整代碼實現 (C語言)
???? ?? 普里姆算法小結
3.克魯斯卡爾算法【Kruskal】
???? ?? 克魯斯卡爾算法簡介
???? ?? 克魯斯卡爾算法完整代碼實現 (C語言)
???? ?? 克魯斯卡爾算法小結
1.最小生成樹
1.1生成樹概念
對連通圖進行遍歷,過程中所經過的邊和頂點的組合可看做是一棵普通樹,通常稱為生成樹。生成樹也理解為包含所有頂點的極小連通子圖(極小連通子圖后邊會講)
連通圖中的生成樹必須滿足以下 2 個條件:
1.包含連通圖中所有的頂點;
2.任意兩頂點之間有且僅有一條通路;
因此,連通圖的生成樹具有這樣的特征,即生成樹中邊的數量 = 頂點數 - 1。
1.2最小生成樹
對于含有 n 個頂點的連通圖來說可能包含有多種生成樹,例如圖 1 所示:
上圖中的連通圖和它相對應的生成樹,可以用于解決實際生活中的問題:假設 A、B、C 和 D 為 4 座城市,為了方便生產生 活,要為這 4 座城市建立通信。對于 4 個城市來講,本著節約經費的原則,只需要建立 3 個通信線路即可,就如圖 1(b) 中的任意一種方式。 在具體選擇采用(b)中哪一種方式時,需要綜合考慮城市之間間隔的距離,建設通信線路的難度等各種因素,將這些因素綜合 起來用一個數值表示,當作這條線路的權值。
假設通過綜合分析,城市之間的權值如圖 2(a)所示,對于(b)的方案中,選擇權值總和為 7 的兩種方案最節約經費。 這就是本節要討論的最小生成樹的問題,簡單得理解就是給定一個帶有權值的連通圖(連通網),如何從眾多的生成樹中篩選出 權值總和最小的生成樹,即為該圖的 最小生成樹
給定一個連通網,求最小生成樹的方法有:普里姆(Prim)算法和克魯斯卡爾(Kruskal)算法。
2.普里姆算法【Prim】
2.1普里姆算法簡介
普里姆算法在找最小生成樹時,將頂點分為兩類:一類是在查找的過程中已經包含在樹中的(假設為 A 類),剩下的是另一類 (假設為 B 類)。
對于給定的連通網,起始狀態全部頂點都歸為 B 類。在找最小生成樹時,選定任意一個頂點作為起始點,并將之從 B 類移至 A
類;然后找出 B 類中到 A 類中的頂點之間權值最小的頂點,將之從 B 類移至 A 類,如此重復,直到 B 類中沒有頂點為止。 所走過的頂點和邊就是該連通圖的最小生成樹。
例如,通過普里姆算法查找上圖的最小生成樹的步驟為: 假如從頂點 A 出發,頂點 B、C、D 到頂點 A 的權值分別為 2、4、2,所以,對于頂點 A 來說,頂點 B 和頂點 D 到 A 的 權值最小,假設先找到的頂點 B:
繼續分析頂點 C 和 D,頂點 C 到 B 的權值為 3,到 A 的權值為 4;頂點 D 到 A 的權值為 2,到 B 的權值為無窮大(如 果之間沒有直接通路,設定權值為無窮大)。所以頂點 D 到 A 的權值最小:
最后,只剩下頂點 C,到 A 的權值為 4,到 B 的權值和到 D 的權值一樣大,為 3。所以該連通圖有兩個最小生成樹:
2.2普里姆算法完整代碼實現 (C語言)
#include <stdio.h> #include <stdlib.h> #define VertexType int #define VRType int #define MAX_VERtEX_NUM 20 #define InfoType char #define INFINITY 65535 typedef struct {VRType adj; //對于無權圖,用 1 或 0 表示是否相鄰;對于帶權圖,直接為權值。InfoType * info; //弧額外含有的信息指針 }ArcCell,AdjMatrix[MAX_VERtEX_NUM][MAX_VERtEX_NUM]; typedef struct {VertexType vexs[MAX_VERtEX_NUM]; //存儲圖中頂點數據AdjMatrix arcs; //二維數組,記錄頂點之間的關系int vexnum,arcnum; //記錄圖的頂點數和弧(邊)數 }MGraph; //根據頂點本身數據,判斷出頂點在二維數組中的位置 int LocateVex(MGraph G,VertexType v){int i=0;//遍歷一維數組,找到變量 vfor (; i<G.vexnum; i++) {if (G.vexs[i]==v) {return i;}}return -1; } //構造無向網 void CreateUDN(MGraph* G){scanf("%d,%d",&(G->vexnum),&(G->arcnum));for (int i=0; i<G->vexnum; i++) {scanf("%d",&(G->vexs[i]));}for (int i=0; i<G->vexnum; i++) {for (int j=0; j<G->vexnum; j++) {G->arcs[i][j].adj=INFINITY;G->arcs[i][j].info=NULL;}}for (int i=0; i<G->arcnum; i++) {int v1,v2,w; scanf("%d,%d,%d",&v1,&v2,&w);int m=LocateVex(*G, v1);int n=LocateVex(*G, v2);if (m==-1 ||n==-1) {printf("no this vertex\n");return;}G->arcs[n][m].adj=w;G->arcs[m][n].adj=w;} } //輔助數組,用于每次篩選出權值最小的邊的鄰接點 typedef struct {VertexType adjvex;//記錄權值最小的邊的起始點VRType lowcost;//記錄該邊的權值 }closedge[MAX_VERtEX_NUM]; closedge theclose;//創建一個全局數組,因為每個函數中都會使用到 //在輔助數組中找出權值最小的邊的數組下標,就可以間接找到此邊的終點頂點。 int minimun(MGraph G,closedge close){int min=INFINITY;int min_i=-1;for (int i=0; i<G.vexnum; i++) { //權值為 0,說明頂點已經歸入最小生成樹中;然后每次和 min 變量進行比較,最后找出最小的。if (close[i].lowcost>0 && close[i].lowcost < min) { min=close[i].lowcost; min_i=i;}}//返回最小權值所在的數組下標return min_i; } //普里姆算法函數,G 為無向網,u 為在網中選擇的任意頂點作為起始點 void miniSpanTreePrim(MGraph G,VertexType u){ //找到該起始點在頂點數組中的位置下標int k=LocateVex(G, u);//首先將與該起始點相關的所有邊的信息:邊的起始點和權值,存入輔助數組中相應的位置,例如(1,2)邊,adjvex 為 0, lowcost 為 6,存入 theclose[1]中,輔助數組的下標表示該邊的頂點 2for (int i=0; i<G.vexnum; i++) {if (i !=k) {theclose[i].adjvex=k;theclose[i].lowcost=G.arcs[k][i].adj;}} //由于起始點已經歸為最小生成樹,所以輔助數組對應位置的權值為 0,這樣,遍歷時就不會被選中theclose[k].lowcost=0; //選擇下一個點,并更新輔助數組中的信息for (int i=1; i<G.vexnum; i++) { //找出權值最小的邊所在數組下標k=minimun(G, theclose); //輸出選擇的路徑printf("v%d v%d\n",G.vexs[theclose[k].adjvex],G.vexs[k]); //歸入最小生成樹的頂點的輔助數組中的權值設為 0theclose[k].lowcost=0; //信息輔助數組中存儲的信息,由于此時樹中新加入了一個頂點,需要判斷,由此頂點出發,到達其它各頂點的權值是否比之前記錄的權值還要小,如果還小,則更新for (int j=0; j<G.vexnum; j++) {if (G.arcs[k][j].adj<theclose[j].lowcost) {theclose[j].adjvex=k;theclose[j].lowcost=G.arcs[k][j].adj;}}}printf("\n"); } int main(){MGraph G;CreateUDN(&G); miniSpanTreePrim(G, 1); }用下圖做例子:
2.3普里姆算法小結
普里姆算法的運行效率只與連通網中包含的頂點數相關,而和網所含的邊數無關。所以普里姆算法適合于解決邊稠密的網,該算法運行的時間復雜度為:O(n2)
如果連通網中所含邊的綢密度不高,則建議使用克魯斯卡爾算法求最小生成樹(下面會講)。
3.克魯斯卡爾算法
3.1克魯斯卡爾算法簡介
對于任意一個連通網的最小生成樹來說,在要求總的權值最小的情況下,最直接的想法就是將連通網中的所有邊按照權值大小進行升序排序,從小到大依次選擇。
由于最小生成樹本身是一棵生成樹,所以需要時刻滿足以下兩點:
- 生成樹中任意頂點之間有且僅有一條通路,也就是說,生成樹中不能存在回路;
- 對于具有 n 個頂點的連通網,其生成樹中只能有 n-1 條邊,這 n-1
所以克魯斯卡爾算法的具體思路是:將所有邊按照權值的大小進行升序排序,然后從小到大一一判斷,條件為:如果這個邊不會與之前選擇的所有邊組成回路,就可以作為最小生成樹的一部分;反之,舍去。直到具有 n 個頂點的連通網篩選出來 n-1 條邊為止。篩選出來的邊和所有的頂點構成此連通網的最小生成樹。
判斷是否會產生回路的方法為:在初始狀態下給每個頂點賦予不同的標記,對于遍歷過程的每條邊,其都有兩個頂點,判斷這兩個頂點的標記是否一致,如果一致,說明它們本身就處在一棵樹中,如果繼續連接就會產生回路;如果不一致,說明它們之間還沒有任何關系,可以連接。假設遍歷到一條由頂點 A 和 B 構成的邊,而頂點 A 和頂點 B 標記不同,此時不僅需要將頂點 A 的標記更新為頂點 B 的標記,還需要更改所有和頂點 A 標記相同的頂點的標記,全部改為頂點 B 的標記。
如下例子,使用克魯斯卡爾算法找最小生成樹的過程為::
首先,在初始狀態下,對各頂點賦予不同的標記(用顏色區別),如下圖所示:
對所有邊按照權值的大小進行排序,按照從小到大的順序進行判斷,首先是(1,3),由于頂點 1 和頂點 3 標記不同,所以 可以構成生成樹的一部分,遍歷所有頂點,將與頂點 3 標記相同的全部更改為頂點 1 的標記,如下:
其次是(4,6)邊,兩頂點標記不同,所以可以構成生成樹的一部分,更新所有頂點的標記為:
其次是(2,5)邊,兩頂點標記不同,可以構成生成樹的一部分,更新所有頂點的標記為:
然后最小的是(3,6)邊,兩者標記不同,可以連接,遍歷所有頂點,將與頂點 6 標記相同的所有頂點的標記更改為頂點 1 的標記:
繼續選擇權值最小的邊,此時會發現,權值為 5 的邊有 3 個,其中(1,4)和(3,4)各自兩頂點的標記一樣,如果連接會產生回路,所以舍去,而(2,3)標記不一樣,可以選擇,將所有與頂點 2 標記相同的頂點的標記全部改為同頂點 3 相同的標記:
當選取的邊的數量相比與頂點的數量小 1 時,說明最小生成樹已經生成。所以最終采用克魯斯卡爾算法得到的最小生成樹為(6)所示
3.2克魯斯卡爾算法完整代碼實現 (C語言)
#include "stdio.h" #include "stdlib.h" #define MAX_VERtEX_NUM 20 #define VertexType int typedef struct edge{VertexType initial;VertexType end;VertexType weight; }edge[MAX_VERtEX_NUM]; //定義輔助數組 typedef struct {VertexType value;//頂點數據int sign;//每個頂點所屬的集合 }assist[MAX_VERtEX_NUM]; assist assists; //qsort 排序函數中使用,使 edges 結構體中的邊按照權值大小升序排序 int cmp(const void *a,const void*b){return ((struct edge*)a)->weight-((struct edge*)b)->weight; } //初始化連通網 void CreateUDN(edge *edges,int *vexnum,int *arcnum){printf("輸入連通網的邊數:\n");scanf("%d %d",&(*vexnum),&(*arcnum));printf("輸入連通網的頂點:\n");for (int i=0; i<(*vexnum); i++) {scanf("%d",&(assists[i].value)); assists[i].sign=i;}printf("輸入各邊的起始點和終點及權重:\n");for (int i=0 ; i<(*arcnum); i++) {scanf("%d,%d,%d",&(*edges)[i].initial,&(*edges)[i].end,&(*edges)[i].weight);} } //在 assists 數組中找到頂點 point 對應的位置下標 int Locatevex(int vexnum,int point){for (int i=0; i<vexnum; i++) {if (assists[i].value==point) {return i;}}return -1; } int main(){int arcnum,vexnum; edge edges;CreateUDN(&edges,&vexnum,&arcnum); //對連通網中的所有邊進行升序排序,結果仍保存在 edges 數組中qsort(edges, arcnum, sizeof(edges[0]), cmp); //創建一個空的結構體數組,用于存放最小生成樹edge minTree; //設置一個用于記錄最小生成樹中邊的數量的常量int num=0; //遍歷所有的邊for (int i=0; i<arcnum; i++) { //找到邊的起始頂點和結束頂點在數組 assists 中的位置int initial=Locatevex(vexnum, edges[i].initial);int end=Locatevex(vexnum, edges[i].end); //如果頂點位置存在且頂點的標記不同,說明不在一個集合中,不會產生回路if (initial!=-1&& end!=-1&&assists[initial].sign!=assists[end].sign) { //記錄該邊,作為最小生成樹的組成部分minTree[num]=edges[i]; //計數+1 num++; //將新加入生成樹的頂點標記全不更改為一樣的for (int k=0; k<vexnum; k++) {if (assists[k].sign==assists[end].sign) {assists[k].sign=assists[initial].sign;} } //如果選擇的邊的數量和頂點數相差 1,證明最小生成樹已經形成,退出循環if (num==vexnum-1) {break; }}} //輸出語句for (int i=0; i<vexnum-1; i++) {printf("%d,%d\n",minTree[i].initial,minTree[i].end);} return 0; }對于下圖例子來說
輸入和運行結果:
3.3克魯斯卡爾算法小結
剛剛介紹了求最小生成樹之普里姆算法。該算法從頂點的角度為出發點,時間復雜度為 O(n2),更適合與解決邊的綢密度更高 的連通網。 本節所介紹的克魯斯卡爾算法,從邊的角度求網的最小生成樹,時間復雜度為 O(eloge)。和普里姆算法恰恰相反,更適合于求 邊稀疏的網的最小生成樹
本篇博客轉載C語言中文網
總結
以上是生活随笔為你收集整理的最小生成树(普里姆算法【Prim】与克鲁斯卡尔算法【Kruskal】)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一文搞定深度优先搜索(DFS)与广度优先
- 下一篇: 回溯算法(八皇后问题)