最近在復習數據結構,所以想起了之前做的一個最小生成樹算法。用Kruskal算法實現的,結合堆排序可以復習回顧數據結構。現在寫出來與大家分享。
最小生成樹算法思想:書上說的是在一給定的無向圖G = (V, E) 中,(u, v) 代表連接頂點 u 與頂點 v 的邊(即),而 w(u, v) 代表此邊的權重,若存在 T 為 E 的子集(即)且為無循環圖,使得的 w(T) 最小,則此 T 為 G 的最小生成樹。說白了其實就是在含有 n 個頂點的連通網中選擇 n-1 條邊,構成一棵極小連通子圖,並使該連通子圖中 n-1 條邊上權值之和達到最小,則稱最小生成樹。
本程序用的是克魯斯卡爾算法(Kruskal),也可以使用prim算法實現。Kruskal思想是在帶權連通圖中,不斷地在排列好的邊集合中找到最小的邊,如果該邊滿足得到最小生成樹的條件,就將其構造,直到最后得到一顆最小生成樹。
圖的頂點存儲結構體:
1 //結構體定義,儲存圖的頂點 2 typedef struct { 3 int from; //邊的起始頂點 4 int to; //邊的終止頂點 5 int cost; //邊的權值 6 }Edge;
問題:頂點編號的類型。
好的程序應該可以擴展,不論頂點用0,1,2... 順序編號還是用5,2,1,7... 亂序編號還是用a,b,c... 英文編號都應該可以做到兼容通過,所以在存儲圖的節點的時候我做了一個映射,就是不論輸入的什么編號一律轉換成順序編號0,1,2...,在最后輸出的時候再把編號映射回原來的編號,這樣就可以應對不同而頂點編號。如下面程序:
1 for(i = 0;i < edgeNum; i++){ 2 printf("請輸入第 %d 條邊!\n",i+1); 3 scanf(" %c %c %d",&from,&to,&cost); 4 edge_temp[i][0] = judge_num(from); 5 edge_temp[i][1] = judge_num(to); 6 edge_temp[i][2] = cost; 7 } 8 //對輸入的邊和點信息進行堆排序 9 HeapSort(edge_temp,edgeNum); 10 int j; 11 for(j = 0;j < edgeNum; j++){ 12 edge[j].from = edge_temp[j][0]; 13 edge[j].to = edge_temp[j][1]; 14 edge[j].cost = edge_temp[j][2]; 15 }
每次輸入頂點后都會先保存到臨時存儲數組edge_temp中,進行堆排序后再把數據白存在真正的數據中。其中判斷是否形成回路借助了一個遞歸方法:
1 //用於判斷是否形成回路 2 int judge(int num){ 3 if(num == judge_temp[num]){ 4 return judge_temp[num]; 5 } 6 return judge_temp[num] = judge(judge_temp[num]); 7 }
執行步驟:
1:在帶權連通圖中,將邊的權值排序(本程序用的是堆排序);
2:判斷是否需要選擇這條邊(此時的邊已按權值從小到大排好序)。判斷的依據是邊的兩個頂點是否已連通,如果連通則繼續下一條;如果不連通,那么就選擇使其連通。
3:循環第二步,直到圖中所有的頂點都在同一個連通分量中,即得到最小生成樹。
判斷法則:(當將邊加入到已找到邊的集合中時,是否會形成回路?)
1:如果沒有形成回路,那么直接將其連通。此時,對於邊的集合又要做一次判斷:這兩個點是否在已找到點的集合中出現過?如果兩個點都沒有出現過,那么將這 兩個點都加入已找到點的集合中;如果其中一個點在集合中出現過,那么將另一個沒有出現過的點加入到集合中;如果這兩個點都出現過,則不用加入到集合中。
2:如果形成回路,不符合要求,直接進行下一次操作。
重點類容就這么多,下面給出源程序,程序直接復制后可以運行,有興趣的朋友也可以用prim算法實現。
1 #include <stdio.h> 2 #include <string.h> 3 //常量定義,邊點最大數量限制50; 4 #define MAXE 52 5 6 /* 7 * Info:最小生成樹算法源碼(C語言版) 8 * @author: zhaoyafei 9 * time: 2015 10 */ 11 12 //結構體定義,儲存圖的頂點 13 typedef struct { 14 int from; //邊的起始頂點 15 int to; //邊的終止頂點 16 int cost; //邊的權值 17 }Edge; 18 19 int nodeNum; //頂點數; 20 int edgeNum; //邊數; 21 int min_cost; //記錄最小生成樹(權值) 22 int judge_temp[MAXE]; //記錄判斷是否成環 23 int sort[MAXE][MAXE]; //用來做排序 24 int edge_temp[MAXE][3]; //用於存儲堆排序邊點信息 25 26 Edge edge[MAXE]; //用於存儲邊點信息 27 Edge min_edge[MAXE]; //用於存儲最小生成樹邊點信息 28 29 char judge_num_int[MAXE]; 30 int inputs = 1; 31 void HeapSort(int array[MAXE][3],int length); 32 int judge_num(char from); 33 34 //save_point()函數,存儲圖的點邊信息; 35 void save_point(){ 36 char from; 37 char to; 38 int cost = 0; 39 int i; 40 for(i = 0;i < edgeNum; i++){ 41 printf("請輸入第 %d 條邊!\n",i+1); 42 scanf(" %c %c %d",&from,&to,&cost); 43 44 edge_temp[i][0] = judge_num(from); 45 edge_temp[i][1] = judge_num(to); 46 edge_temp[i][2] = cost; 47 } 48 //對輸入的邊和點信息進行堆排序 49 HeapSort(edge_temp,edgeNum); 50 int j; 51 for(j = 0;j < edgeNum; j++){ 52 edge[j].from = edge_temp[j][0]; 53 edge[j].to = edge_temp[j][1]; 54 edge[j].cost = edge_temp[j][2]; 55 } 56 } 57 58 int judge_num(char str){ 59 int n1 = 0; 60 for(int j1 = 1;j1 < edgeNum * 2; j1++){ 61 if(str == judge_num_int[j1]){ 62 n1++; 63 } 64 } 65 if(n1 == 0){ 66 judge_num_int[inputs] = str; 67 inputs++; 68 } 69 int return_num = 1; 70 for(int j2 = 1;j2 < edgeNum * 2; j2++){ 71 if(str == judge_num_int[j2]){ 72 return_num = j2; 73 } 74 } 75 return return_num; 76 } 77 78 //用於判斷是否形成回路 79 int judge(int num){ 80 if(num == judge_temp[num]){ 81 return judge_temp[num]; 82 } 83 return judge_temp[num] = judge(judge_temp[num]); 84 } 85 86 //判斷是否是一棵最小生成樹 87 bool is_judge(){ 88 int oneedge = judge(1); 89 int i; 90 for(i = 2;i <= nodeNum; i++) { 91 if(oneedge != judge(i)){ 92 return false; 93 } 94 } 95 return true; 96 } 97 98 //kruskal算法 99 void kruskal(){ 100 min_cost = 0;//最小生成樹 101 //初始化輔助回路判斷數組 102 int m; 103 for(m = 0;m < MAXE;m++) { 104 judge_temp[m] = m; 105 } 106 107 int edge_num = 0;//記錄最小生成樹的邊數 108 int i; 109 for(i = 0;i < edgeNum; i++){ 110 //小於總節點數 111 if(edge_num != nodeNum - 1){ 112 int edge_from = judge(edge[i].from); 113 int edge_to = judge(edge[i].to); 114 //如果形成回路則edge_from == edge_to; 115 if(edge_from != edge_to){ 116 //如果沒有形成回路,則改變原臨時數組中的值 117 judge_temp[edge_from] = edge_to; 118 min_cost += edge[i].cost; 119 120 //將符合的邊加入到存儲數組中 121 min_edge[edge_num].from = edge[i].from; 122 min_edge[edge_num].to = edge[i].to; 123 min_edge[edge_num].cost = edge[i].cost; 124 125 edge_num++; 126 } 127 } 128 } 129 } 130 131 //array是待調整的堆數組,i是待調整的數組元素的位置,nlength是數組的長度 132 //根據數組array構建大頂堆 133 void HeapAdjust(int array[MAXE][3],int i,int nLength){ 134 int nChild; 135 for(; 2*i + 1 < nLength; i = nChild){ //子結點的位置=2*(父結點位置)+1 136 nChild = 2*i + 1; 137 //得到子結點中較大的結點 138 if(nChild < nLength-1 && array[nChild+1][2] > array[nChild][2]){ 139 ++nChild; 140 } 141 //如果較大的子結點大於父結點那么把較大的子結點往上移動,替換它的父結點 142 if(array[i][2] < array[nChild][2]){ 143 int temp_arr2[3]; 144 temp_arr2[0] = array[i][0]; 145 temp_arr2[1] = array[i][1]; 146 temp_arr2[2] = array[i][2]; 147 148 array[i][0] = array[nChild][0]; 149 array[i][1] = array[nChild][1]; 150 array[i][2] = array[nChild][2]; 151 152 array[nChild][0] = temp_arr2[0]; 153 array[nChild][1] = temp_arr2[1]; 154 array[nChild][2] = temp_arr2[2]; 155 }else{ 156 break;//否則退出循環 157 } 158 } 159 } 160 161 //堆排序算法 162 void HeapSort(int array[MAXE][3],int length){ 163 //調整序列的前半部分元素,調整完之后第一個元素是序列的最大的元素 164 //length/2-1是最后一個非葉節點,此處"/"為整除 165 int j; 166 for( j= length/2 - 1; j >= 0; --j){ 167 HeapAdjust(array,j,length); 168 } 169 //從最后一個元素開始對序列進行調整,不斷的縮小調整的范圍直到第一個元素 170 int i; 171 for(i = length - 1; i > 0; --i){ 172 //把第一個元素和當前的最后一個元素交換, 173 //保證當前的最后一個位置的元素都是在現在的這個序列之中最大的 174 int temp_arr1[3]; //構建二維數組的原因:交換后保證數組中其他屬性值同時交換 175 temp_arr1[0] = array[i][0]; 176 temp_arr1[1] = array[i][1]; 177 temp_arr1[2] = array[i][2]; 178 179 array[i][0] = array[0][0]; 180 array[i][1] = array[0][1]; 181 array[i][2] = array[0][2]; 182 183 array[0][0] = temp_arr1[0]; 184 array[0][1] = temp_arr1[1]; 185 array[0][2] = temp_arr1[2]; 186 187 //不斷縮小調整heap的范圍,每一次調整完畢保證第一個元素是當前序列的最大值 188 HeapAdjust(array,0,i); 189 } 190 } 191 192 //輸出最小生成樹的信息(包括邊點和權值) 193 void output(){ 194 if(is_judge()){ 195 printf("最小生成樹:\n"); 196 printf("起點 -> 終點 路徑長:\n"); 197 for(int i = 0;i < nodeNum-1; i++){ 198 printf(" %c -> %c %d\n",judge_num_int[min_edge[i].from],judge_num_int[min_edge[i].to],min_edge[i].cost); 199 } 200 printf("min cost is : %d\n",min_cost); 201 printf("*******************************************************************************\n"); 202 printf("請輸入 節點數 邊數(中間需用空格隔開):\n"); 203 }else{ 204 printf("最小生成樹不存在!\n"); 205 printf("*******************************************************************************\n"); 206 printf("請輸入 節點數 邊數(中間需用空格隔開):\n"); 207 } 208 } 209 210 /* 211 * 程序主方法; 212 * 用於開始程序,介紹程序操作須知; 213 */ 214 int main(){ 215 printf("*******************************************************************************\n"); 216 printf("** 最小生成樹(kruskal算法) ***\n"); 217 printf("** 說明:開始程序輸入圖的總點數和總邊數,測試程序目前邊點限制最多輸入50個 ***\n"); 218 printf("** 中間用空格隔開。輸入邊和點信息,需要按格式:開始邊 終止邊 權值 ***\n"); 219 printf("** 本次計算結束可以按要求開始下次計算。 ***\n"); 220 printf("*******************************************************************************\n"); 221 printf("請輸入 節點數 邊數(中間需用空格隔開):\n"); 222 while(scanf("%d%d",&nodeNum,&edgeNum) != EOF){ 223 //判斷輸入的邊和點的合法性 224 if(nodeNum < 1 || edgeNum < 1){ 225 printf("輸入的數據不合法\n"); 226 printf("請輸入 節點數 邊數(中間需用空格隔開):\n"); 227 return 0; 228 }else if(nodeNum > 50 || edgeNum > 50){ 229 printf("輸入的邊或者點不能大於50\n"); 230 printf("請輸入 節點數 邊數(中間需用空格隔開):\n"); 231 return 0; 232 }else{ 233 printf("請輸入 開始節點 終止節點 該邊的權值(中間需用空格隔開,回車換行):\n"); 234 printf("共 %d 條邊\n",edgeNum); 235 for(int m = 0;m < MAXE; m++) { 236 judge_num_int[m] = '-'; 237 } 238 inputs = 1; 239 save_point(); //存儲邊點信息 240 kruskal(); //算法執行 241 output(); //輸出執行結果 242 } 243 } 244 return 0; 245 }
運行結果如下: