最小生成樹
列子引入
如圖假設
v0
到v8
表示9個村莊,現在需要在這9個村莊假設通信網絡。村莊之間的數字代表村莊之間的直線距離,求用最小成本完成這9個村莊的通信網絡建設。
分析
- 這幅圖只一個帶權值的圖,即網結構。
- 所謂最小成本,就是
n
個頂點,用n-1
條邊把一個連通圖連接起來,并且使權值的和最小。
最小生成樹
如果無向連通圖是一個網圖,那么它的所有生成樹中必有一顆是邊的權值總和最小的生成樹,即最小生成樹。
找到連通圖的最小生成樹,有兩種經典的算法:普里姆(Prim)算法和克魯斯卡爾(Kruskal)算法
一、普里姆(Prim)算法
圖的鄰接矩陣
普利姆算法步驟
- 從圖中某一個頂點出發(這里選
V0
),尋找它相連的所有結點,比較這些結點的權值大小,然后連接權值最小的那個結點。(這里是V1
) - 然后將尋找這兩個結點相連的所有結點,找到權值最小的連接。(這里是
V5
). -
重復上一步,知道所有結點都連接上。
最小生成樹
實現代碼
#include <stdio.h>
#include <stdlib.h>
#define MAXEDGE 20
#define MAXVEX 20
#define INIFINTY 65535
typedef struct {
int arc[MAXVEX][MAXVEX];
int numVertexes, numEdges;
}MGraph;
/**
* 構建圖
*/
void CreateMGraph(MGraph * G){
int i, j;
G->numVertexes = 9; // 9個頂點
G->numEdges = 15; // 15條邊
for (i = 0; i < G->numVertexes; i++) { // 初始化圖
for (j = 0; j < G->numVertexes; j++) {
if (i == j)
G->arc[i][j] = 0;
else
G->arc[i][j] = G->arc[j][i] = INIFINTY;
}
}
G->arc[0][1] = 10;
G->arc[0][5] = 11;
G->arc[1][2] = 18;
G->arc[1][8] = 12;
G->arc[1][6] = 16;
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 (i = 0; i < G->numVertexes; i++)
for (j = 0; j < G->numVertexes; j++)
G->arc[j][i] = G->arc[i][j];
}
/**
* Prime算法生成最小生成樹
*/
void MiniSpanTree_Prim(MGraph G){
int min,i,j,k;
int adjvex[MAXVEX]; // 保存相關頂點的下標
int lowcost[MAXVEX]; // 保存相關頂點間邊的權值
lowcost[0] = 0; // 初始化第一個權值為0,即v0加入生成樹
adjvex[0] = 0; // 初始化第一個頂點下標為0
for (i = 1; i < G.numVertexes; i++) { // 循環除下標為0外的全部頂點
lowcost[i] = G.arc[0][i]; // 將v0頂點與之右邊的權值存入數組
adjvex[i] = 0; // 初始化都為v0的下標
}
for (i = 1; i < G.numVertexes; i++) {
min = INIFINTY; //初始化最小權值
j = 1;
k = 0;
while (j < G.numVertexes) { // 循環全部頂點
if (lowcost[j] != 0 && lowcost[j] < min) {
min = lowcost[j]; // 讓當前權值變為最小值
k = j; // 將當前最小值的下標存入k
}
j++;
}
printf("(%d, %d)\n", adjvex[k], k); // 打印當前頂點中權值最小的邊
lowcost[k] = 0; // 將當前頂點的權值設置為0,表示此頂點已經完成任務
for (j = 1; j < G.numVertexes; j++) { // 循環所有頂點
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(int argc, const char * argv[]) {
MGraph G;
CreateMGraph(&G);
MiniSpanTree_Prim(G);
return 0;
}
代碼解釋
- 創建了兩個數組
adjvex
和lowcost
。adjvex[0] = 0
意思就是從V0
開始,lowcost[0] = 0
表示V0
已經被納入到最小生成樹中。之后凡是lowcost
數組中的值被設置為0就是表示此下標的頂點被納入最小生成樹。 - 普里姆算法的時間復雜度為
O(n^2)
,因為是兩層循環嵌套。
代碼運行結果
普里姆算法運行結果
二、克魯斯卡爾(Kruskal)算法
普里姆算法是從某一頂點為起點,逐步找各個頂點最小權值的邊來構成最小生成樹。那我們也可以直接從邊出發,尋找權值最小的邊來構建最小生成樹。不過在構建的過程中要考慮是否會形成環的情況
邊集數組存儲圖
邊集數組
在直接用邊來構建最小生成樹的時候,需要用到邊集數組結構,代碼為:
typedef struct { // 邊集數組
int begin;
int end;
int weight;
}Edge;
代碼實現
#include <stdio.h>
#include <stdlib.h>
#define MAXEDGE 20
#define MAXVEX 20
#define INIFINTY 65535
typedef struct {
int arc[MAXVEX][MAXVEX];
int numVertexes, numEdges;
}MGraph;
typedef struct { // 邊集數組
int begin;
int end;
int weight;
}Edge;
/**
* 構建圖
*/
void CreateMGraph(MGraph * G){
int i, j;
G->numVertexes = 9; // 9個頂點
G->numEdges = 15; // 15條邊
for (i = 0; i < G->numVertexes; i++) { // 初始化圖
for (j = 0; j < G->numVertexes; j++) {
if (i == j)
G->arc[i][j] = 0;
else
G->arc[i][j] = G->arc[j][i] = INIFINTY;
}
}
G->arc[0][1] = 10;
G->arc[0][5] = 11;
G->arc[1][2] = 18;
G->arc[1][8] = 12;
G->arc[1][6] = 16;
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 (i = 0; i < G->numVertexes; i++)
for (j = 0; j < G->numVertexes; j++)
G->arc[j][i] = G->arc[i][j];
}
/**
* 交換權值、頭、尾
*/
void Swapn(Edge * edges, int i, int j){
int temp;
temp = edges[i].begin;
edges[i].begin = edges[j].begin;
edges[j].begin = temp;
temp = edges[i].end;
edges[i].end = edges[j].end;
edges[j].end = temp;
temp = edges[i].weight;
edges[i].weight = edges[j].weight;
edges[j].weight = temp;
}
/**
* 對權值進行排序
*/
void sort(Edge edges[], MGraph *G){
int i,j;
for (i = 0; i < G->numEdges; i++) {
for (j = i+1; j < G->numEdges; j++) {
if (edges[i].weight > edges[j].weight)
Swapn(edges, i, j);
}
}
printf("權值排序之后為:\n");
for (i = 0; i < G->numEdges; i++) {
printf("(%d, %d) %d\n", edges[i].begin, edges[i].end, edges[i].weight);
}
}
/**
* 查找連線頂點的尾部下標
*/
int Find(int * parent, int f){
while (parent[f] > 0)
f = parent[f];
return f;
}
void MiniSpanTree_Kruskal(MGraph G){
int i,j,n,m;
int k = 0;
Edge edges[MAXEDGE]; // 定義邊集數組
int parent[MAXVEX]; // 定義一維數組來判斷邊與邊是否形成回路
//構建邊集數組并排序
for (i = 0; i < G.numVertexes - 1; i++) {
for (j = i+1; j < G.numVertexes; j++) {
if (G.arc[i][j] < INIFINTY) {
edges[k].begin = i;
edges[k].end = j;
edges[k].weight = G.arc[i][j];
k++;
}
}
}
sort(edges, &G);
for (i = 0; i < G.numVertexes; i++) {
parent[i] = 0;
}
printf("打印最小生成樹:\n");
for (i = 0; i < G.numEdges; i++) {
n = Find(parent, edges[i].begin);
m = Find(parent, edges[i].end);
if (n != m) {
parent[n] = m;
printf("(%d, %d) %d\n",edges[i].begin, edges[i].end
, edges[i].weight);
}
}
}
int main(int argc, const char * argv[]) {
MGraph G;
CreateMGraph(&G);
MiniSpanTree_Kruskal(G);
return 0;
}
代碼解釋
- 先構建邊集數組,并排序,所以前面有對權值進行排序的方法
sort
。 - 克魯斯卡爾(Kruskal)算法的時間復雜度為
O(eloge)
。
運行結果
對比普里姆(Prim)算法和克魯斯卡爾(Kruskal)算法
- 克魯斯卡爾(Kruskal)算法主要針對邊來展開,邊數較少時效率非常高,所以對于稀疏圖有很大的優勢;
- 普里姆(Prim)算法對于稠密圖,邊數非常多的情況更好一些。