一:回顧普里姆算法
普里姆算法是以某個頂點為起點,逐步找各頂點上最小權值的邊來構建最小生成樹。是臨時決定路徑。
例如:我們參觀某個展會,從一個入口進入,然后我們會選擇最感興趣的場館進入觀看,看完后再用同樣的方法看下一個。
二:克魯斯卡爾算法(稀疏圖)
推文:https://www.cnblogs.com/qianbixin/p/5005161.html(轉載自)
例如上面參觀,我們為何不先決定去看那些展館,事先計划好,然后進園后直接按照所決定的路徑進行觀看。這就是克魯斯卡爾算法的思想
注意:
克魯斯卡爾算法需要我們了解生成樹的概念,我們可以回到普里姆算法回顧下
(一)基本思想
(1)構造一個只含n個頂點,邊集為空的子圖。若將圖中各個頂點看成一棵樹的根節點,則它是一個含有n棵樹的森林。 (2)從網的邊集 E 中選取一條權值最小的邊,若該條邊的兩個頂點分屬不同的樹,則將其加入子圖。也就是說,將這兩個頂點分別所在的兩棵樹合成一棵樹;反之,若該條邊的兩個頂點已落在同一棵樹上,則不可取,而應該取下一條權值最小的邊再試之 (3)依次類推,直至森林中只有一棵樹,也即子圖中含有 n-1條邊為止。
或者
(1)將圖中的所有邊都去掉。
(2)將邊按權值從小到大的順序添加到圖中,保證添加的過程中不會形成環
(3)重復上一步直到連接所有頂點,此時就生成了最小生成樹。這是一種貪心策略。
(二)難點
判斷某條邊<u, v>的加入是否會在已經選定的邊集集合中形成環。
(三)解決思路
使用並查集,分別找出兩個頂點u, v所在樹的根節點。若根節點相同,說明u, v在同一棵樹中,則u, v連接起來會形成環;若根節點不同,則u, v不在一棵樹中,連接起來不會形成環,而是將兩棵樹合並。
推文:數據結構(四)樹---集合的表示及查找(並查集)
(四)圖解過程


三:代碼實現
數據結構定義
#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; typedef struct { int begin; int end; int weight; }Edge;
函數定義
//創建鄰接矩陣 void CreateMGraph(MGraph* G); //鄰接矩陣轉邊集數組 void MGraph2EdgeArr(MGraph G, Edge* edge); //顯示鄰接矩陣 void showGraph(MGraph G); //找到頂點index的根節點下標返回 int Find(int* parent, int index); //使用克魯斯卡爾算法進行最小生成樹的創建 void MiniSpanTree_Kruskal(MGraph G);
創建鄰接矩陣
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 MGraph2EdgeArr(MGraph G, Edge* edge) { int i, j,k=0; Edge temp; int min; for (i = 0; i < G.numVertexes;i++) { for (j = i + 1; j < G.numVertexes;j++) { if (G.arc[i][j]!=INFINITY) //有邊 { edge[k].begin = i; edge[k].end = j; edge[k].weight = G.arc[i][j]; k++; } } } //按照冒泡大小進行排序 for (i = 0; i < k;i++) { for (j = i; j < k;j++) { if (edge[j].weight<edge[i].weight) { temp = edge[i]; edge[i] = edge[j]; edge[j] = temp; } } } }
並查集操作,獲取一個頂點f的根節點下標,這里沒有使用結構體,而是將數組下標作為了數據,節省了不必要空間
int Find(int* parent, int f) { while (parent[f] > 0) f = parent[f]; return f; }
使用克魯斯卡爾算法進行最小生成樹的創建
void MiniSpanTree_Kruskal(MGraph G) { Edge edges[MAXVEX]; //定義邊集數組 int parent[MAXVEX]; //定義生成樹的父節點,也可以使用結構體,但是更加浪費空間 int i,n,m; MGraph2EdgeArr(G, edges); //鄰接矩陣轉邊集數組 //開始進行初始化 for (i = 0; i < MAXVEX; i++) parent[i] = 0; //這里是0代表根節點,我們也可以使用-1,正負無窮等 //進行合並操作 for (i = 0; i < G.numEdges;i++) { n = Find(parent, edges[i].begin); //找到頂點edges[i].begin的根節點下標 m = Find(parent, edges[i].end); //找到頂點edges[i].end的根節點位置 if (n!=m) //若是根節點下標不是一樣的,就說不在一棵樹上,不會形成環,我們放心合並 { parent[n] = m; //將n樹合並到m樹,表示該邊被放入生成樹 printf("(%d,%d) %d ", edges[i].begin, edges[i].end, edges[i].weight); } } }
全部代碼
#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; typedef struct { int begin; int end; int weight; }Edge; //創建鄰接矩陣 void CreateMGraph(MGraph* G); //鄰接矩陣轉邊集數組 void MGraph2EdgeArr(MGraph G, Edge* edge); //顯示鄰接矩陣 void showGraph(MGraph G); //找到頂點index的根節點下標返回 int Find(int* parent, int index); //使用克魯斯卡爾算法進行最小生成樹的創建 void MiniSpanTree_Kruskal(MGraph G); int main() { MGraph MG; CreateMGraph(&MG); showGraph(MG); MiniSpanTree_Kruskal(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 MGraph2EdgeArr(MGraph G, Edge* edge) { int i, j,k=0; Edge temp; int min; for (i = 0; i < G.numVertexes;i++) { for (j = i + 1; j < G.numVertexes;j++) { if (G.arc[i][j]!=INFINITY) //有邊 { edge[k].begin = i; edge[k].end = j; edge[k].weight = G.arc[i][j]; k++; } } } //按照冒泡大小進行排序 for (i = 0; i < k;i++) { for (j = i; j < k;j++) { if (edge[j].weight<edge[i].weight) { temp = edge[i]; edge[i] = edge[j]; edge[j] = temp; } } } } //顯示鄰接矩陣 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"); } } //並查集操作,獲取一個頂點f的根節點下標 int Find(int* parent, int f) { while (parent[f] > 0) f = parent[f]; return f; } //使用克魯斯卡爾算法進行最小生成樹的創建 void MiniSpanTree_Kruskal(MGraph G) { Edge edges[MAXVEX]; //定義邊集數組 int parent[MAXVEX]; //定義生成樹的父節點,也可以使用結構體,但是更加浪費空間 int i, n, m; MGraph2EdgeArr(G, edges); //鄰接矩陣轉邊集數組 //開始進行初始化 for (i = 0; i < MAXVEX; i++) parent[i] = 0; //這里是0代表根節點,我們也可以使用-1,正負無窮等 //進行合並操作 for (i = 0; i < G.numEdges; i++) { n = Find(parent, edges[i].begin); //找到頂點edges[i].begin的根節點下標 m = Find(parent, edges[i].end); //找到頂點edges[i].end的根節點位置 if (n != m) //若是根節點下標不是一樣的,就說不在一棵樹上,不會形成環,我們放心合並 { parent[n] = m; //將n樹合並到m樹,表示該邊被放入生成樹 printf("(%d,%d) %d ", edges[i].begin, edges[i].end, edges[i].weight); } } }

四:總結
將圖中各邊按其權值由小到大的次序順序選取,若選邊后不形成回路,則保留作為一條邊, 若形成回路則除去.依次選夠(n-1)條邊,即得最小生成樹.(n為頂點數)
克魯斯卡爾算法的時間復雜度為O(eloge)跟邊的數目有關,適合稀疏圖。(若圖的頂點數為n,邊數為e)。
