基於遺傳算法的TSP問題求解(C)
TSP問題:
TSP(Travelling salesman problem): 譯作“旅行商問題”, 一個商人由於業務的需要,要到n個城市,每個城市之間都有一條路徑和其他所有的城市相連。現在要求從一個城市出發,穿越所有其他所有的城市,再回到出發的城市。 出於成本的考慮,要求商人走的路徑的長短最短。問能否找到這樣的一條路徑?
這是個經典的NP-complete問題。 時間復雜度為θ(n!)。 隨着城市的數量規模增大,在有限的時間內得不到問題的最優解。 我們只能退而求其次,在有限的時間內(或可接受的時間內)找到一個近似最優解,而且這個近似最優解和最優解的誤差我們也是可以接受的。 出於這樣的考慮,為求解這類問題的啟發式,元啟發式算法,演化算法營運而生。
隨着研究的深入,TSP問題已經演化成好多版本。本文的C程序對於對稱和非對稱的版本都適用。
遺傳算法:
遺傳算法(Genetic Algorithm),也稱進化算法,是依據生物進化的過程而提出的一種啟發式算法,由美國的J.Holland於1975年首次提出。其主要特點是依據生物進化中的“優勝劣汰”或者“競爭”法作為問題的解的選擇依據。 直接對結構對象進行操作,不存在求導和函數連續性的限定;具有內在的隱並行性和更好的全局尋優能力;采用概率化的尋優方法,能自動獲取和指導優化的搜索空間,自適應地調整搜索方向,不需要確定的規則。遺傳算法的這些性質,已被人們廣泛地應用於組合優化、機器學習、信號處理、自適應控制和人工生命等領域。它是現代有關智能計算中的關鍵技術之一
遺傳算法的基本原理:
首先根據問題產生多個初始可行解(),然后從初始解中選擇諾干優異的個體(問題的解)進行各種操作(交叉,變異)用以產生新的后代。再從新的后代中選擇優異的個體進行相應的操作產生它們的后代,如此不斷循環,直到迭代的次數達到了預先設定的值或者多次迭代以后產生的最優異的個體(最優解)的質量並沒有明顯的提高,就可以停止迭代,這時的最優個體(最優解)最為問題的解。
從上面的原理我們可以知曉該算法主要涉及的步驟如圖 1所示:
- 編碼
在解實現初始化之前,如何表示一個解即編碼是一個很重要的問題。 一般的編碼方式有:
- 基於二進制編碼: 作為遺傳算法傳統的編碼方式。對於TSP問題,經過交叉后可能得不到可行解,需要修補操作。
- 基於矩陣的編碼: 需要更多的額外空間,而且隨着種群規模的增加,存儲空間劇增和前期處理工作任務很龐大。后續操作也比較復雜。
- 基於鄰近的編碼: 采用鏈表的方式存儲,但是變異,交叉操作復雜
- 基於格雷碼方法:傳統二進制編碼的一種改進,容易實現交叉,變異操作,但是對於該問題不是最優的
- 基於符號編碼:對於TSP問題,我們直接使用路徑來表示一個染色體。即使用一個整數數組,數組長度為TSP城市的數量,數組中存儲各個城市編號,從下標為0開始逐個遍歷數組元素形成的一個序列即為路徑(對於要回到原點的要求,為了方便表示,在這里不作考慮,只是在計算路徑長度的時候會添加相應的長度)。
- 形成初始解
采用隨機的方式產生諾干個(種群規模)的序列,即產生符合城市編號的隨機數存儲在數組中,數組中的元素包含所有的城市編號,而且沒有重復的編碼。數組的個數為種群規模。
- 選擇
在形成一定數量的可行解(染色體)后,需要對這些父代的染色體進行篩選。根據生物遺傳的基因的優勝劣汰原則,在篩選染色體的我們也會秉承精英保留原則,使得優異的基因有更多的機會得到遺傳。
適應度函數: 這里我們選擇路徑長度的倒數來作為解的適應度
在這個問題中,我們選擇“輪盤賭”算法來篩選染色體。
基本原理:計算每個染色體(路徑)的長度的倒數,再得到所有路徑倒數之和,用每條路徑的倒數除以所有所有路徑倒數之和即為這條路徑被選中的概率(路徑越短,概率越高)。
- 交叉
這里我們選擇交替位置交叉法(Alternating Position Crossover,APX)來對一對染色體進行交叉操作,其基本原理如下圖所示
左邊為父代的兩個染色體,右邊為子代染色體。 將左上的數組第一個元素放入右上數組的第一位置中,再轉移到左下數組第一個元素,查看右上數組是否已經包含了該元素,如果未包含將其插入右上數組中,否則插入右下數組中。接着從左上數組的第二個元素開始,到左下第二個元素,和前次同意的判斷操作。如此類推直到右邊兩個數組都被填滿了為止。
交叉概率:交叉概率對於解的收斂速度有着重要的影響。一般選擇0.6-1。
- 變異
生物的進化,除了遺傳父母的基因,還有自身基因有一定的概率突變。基於這個原理,變異操作在一定的概率上是作用於染色體自身的。
變異概率:一定的概率師兄自身基因的改變
在這個問題中,我們選擇位置倒換法,即染色體上隨機的產生兩個位置上數值互換。
- 終止條件
一般有兩種方式停止交叉,變異的操作。一,預先設定迭代次數。二,多次跌代后,解的質量得不到一定要求的提高,或者解達到要求的質量,這時都可以停止迭代。這個問題上我們選擇第一種。
基於TSP問題的遺傳算法代碼如下:
1 #include <stdio.h> 2 #include <tchar.h> 3 #include <math.h> 4 #include <stdlib.h> 5 #include <time.h> 6 7 8 int scale; /* 種群規模 */ 9 int cityNum; /* 城市數量 */ 10 int *pathlength; /* 存儲種群每個個體路徑長度 */ 11 double *cumPropa; /* 存儲個體累積概率 */ 12 int bestlength; /* 最佳路徑長度 */ 13 int *bestpath; /* 最佳路徑 */ 14 float pc; /* 交叉概率 */ 15 float pm; /* 變異概率 */ 16 int count; /* 變異次數 */ 17 int MAX_Gene; /* 迭代次數 */ 18 19 /* 函數聲明 */ 20 void APCrossover(int **,int ,int ,int ); 21 void copy(int *,int **,int ,int ); 22 void cumDistance(int **,int **, int *,int ,int ); 23 void cumulatePro(int **,double *,int *,int ); 24 void copytoBestPath(int *,int **,int ,int ); 25 void copy(int *,int **,int ,int ); 26 int *creatArray1(int ); 27 double *creatArraydoub(int ); 28 int **creatArray2(int , int ); 29 void CrossAndMutation(int **,int ); 30 void cumDistance(int **,int **, int *,int ,int); 31 void mutation(int **,int); 32 void Initialize(int **, int , int ); 33 void readfile(int **, int , int ); 34 void rouletteAlgo(int **,double *, int **,int ); 35 void NcopyO(int **,int **); 36 37 //創建一個整形的二維整數數組 38 int **creatArray2(int scale, int cityNum) 39 { 40 int i; 41 42 int **ptemp; 43 44 ptemp=(int **)malloc(sizeof(int *)*scale); 45 for(i=0;i<scale;i++) 46 ptemp[i]=(int *)malloc(sizeof(int)*cityNum); 47 48 return ptemp; 49 } 50 //創建一個整形的一維數組 51 int *creatArray1(int scale) 52 { 53 int *tempcreate; 54 tempcreate=(int *)malloc(sizeof(int)*scale); 55 return tempcreate; 56 } 57 //創建一個雙精度型的一維數組 58 double *creatArraydoub(int scale) 59 { 60 double *tempcreate; 61 tempcreate=(double *)malloc(sizeof(double)*scale); 62 return tempcreate; 63 } 64 65 // 二維數組拷貝 66 void NcopyO(int **newgeneration,int **oldgeneratoin) 67 { 68 int i,j; 69 for(i=0;i<scale;i++) 70 for(j=0;j<cityNum;j++) 71 oldgeneratoin[i][j]=newgeneration[i][j]; 72 73 } 74 75 76 77 //計算一次迭代各可行解的路徑長度 78 void cumDistance(int **oldgeneration,int **cityDistance, int *pathlength,int cityNum,int scale) 79 { 80 int i,j,k; 81 int length=0; 82 int min=88888; 83 int minp; 84 85 for(i=0;i<scale;i++) 86 { 87 length=0; 88 for(k=0;k<cityNum-1;k++) 89 { 90 j=k+1; 91 length+=cityDistance[oldgeneration[i][k]][oldgeneration[i][j]]; 92 } 93 //題目要求回到原點,所以加上回到原點的距離 94 pathlength[i]=length+cityDistance[oldgeneration[i][cityNum-1]][oldgeneration[i][0]]; 95 96 if(pathlength[i]<min) 97 { 98 min=pathlength[i]; 99 minp=i; 100 } 101 } 102 if(min<bestlength) 103 { 104 bestlength=min; 105 //將每代最優解直接加入下一代中,即“精英保留”原則 106 copytoBestPath(bestpath,oldgeneration,minp,cityNum); 107 } 108 109 } 110 111 112 113 114 void copytoBestPath(int *bestpath,int **oldGeneration,int position,int cityNum) 115 { 116 int i; 117 for(i=0;i<cityNum;i++) 118 bestpath[i]=oldGeneration[position][i]; 119 } 120 121 122 123 //計算一次迭代中各個可行解作為交叉操作的累積概率 124 125 void cumulatePro(int **generation,double *cumPropa,int *pathlength,int scale) 126 { 127 int i; 128 double sumlength=0; 129 130 for(i=0;i<scale;i++) 131 sumlength+=1/(double)pathlength[i]; 132 133 cumPropa[0]=(1/(double)pathlength[0])/sumlength; 134 135 for(i=1;i<scale;i++) 136 { 137 cumPropa[i]=(1/(double)pathlength[i])/sumlength+cumPropa[i-1]; 138 } 139 } 140 141 142 //初始化話生產相應規模的可行解 143 void Initialize(int **geneSolution, int scale, int cityNum) 144 { 145 int i,j,k; 146 int randnum; 147 bool exist; 148 149 srand(time(NULL)); 150 151 for(i=0;i<scale;i++) 152 for(j=0;j<cityNum;j++) 153 { 154 exist=true; 155 while(exist==true){ 156 //注意:rand()/Rand_MAX 結果只能是0, 應該先進行類型轉換 157 randnum=(int)(((double)rand()/RAND_MAX)*cityNum); 158 for(k=0;k<j;k++) 159 if(geneSolution[i][k]==randnum) 160 break; 161 if(k==j) 162 exist=false; 163 } 164 geneSolution[i][j]=randnum; 165 } 166 167 } 168 169 //讀取文件中的各相鄰點的距離信息 170 void readfile(int **cityDistance, int scale, int cityNum) 171 { 172 int i,j; 173 FILE *fp; 174 errno_t err; 175 176 err=fopen_s(&fp,"data.txt","r"); 177 178 if(err!=0) 179 { 180 printf("The file can not be found!\n"); 181 } 182 else 183 { 184 185 for(i=0;i<scale;i++) 186 { 187 for(j=0;j<cityNum;j++) 188 { 189 fscanf_s(fp,"%d ",&cityDistance[i][j]); 190 } 191 } 192 193 194 195 fclose(fp); 196 } 197 198 } 199 200 //拷貝一條路徑 201 void copy(int *oldGeneration,int **newGeneration,int position,int cityNum) 202 { 203 int i; 204 for(i=0;i<cityNum;i++) 205 newGeneration[position][i]=oldGeneration[i]; 206 } 207 208 //使用輪盤賭算法選擇交叉的對象 209 void rouletteAlgo(int **oldGeneration,double *cumPropa, int **newGeneration,int scale) 210 { 211 int i,j; 212 double randNum; 213 214 for(i=0;i<scale-1;i++) 215 { 216 randNum=(double)rand()/RAND_MAX; 217 if(cumPropa[0]>=randNum) 218 copy(oldGeneration[0],newGeneration,i,scale); 219 else 220 { 221 for(j=0;j<scale;j++) 222 if(randNum>cumPropa[j] && randNum<=cumPropa[j]) 223 break; 224 copy(oldGeneration[i],newGeneration,i,scale); 225 } 226 } 227 228 copy(bestpath,newGeneration,scale-1,scale); 229 230 } 231 232 //交叉操作:交替位置交叉法(Alternating Position Crossover,APX) 233 void APCrossover(int **newgeneration,int p1,int p2,int cityNum) 234 { 235 int i; 236 int s1=1; 237 int s2=0; 238 239 int *tempArray1; 240 int *tempArray2; 241 242 tempArray1=creatArray1(cityNum); 243 tempArray2=creatArray1(cityNum); 244 245 for(i=0;i<cityNum;i++) 246 { 247 tempArray1[i]=newgeneration[p1][i]; 248 tempArray2[i]=newgeneration[p2][i]; 249 } 250 251 int m,n; 252 m=1; 253 n=0; 254 255 while(s1<10 || s2<10) 256 { 257 for(i=0;i<s1;i++) 258 if(tempArray1[m]==newgeneration[p1][i]) 259 break; 260 if(i==s1) 261 { 262 newgeneration[p1][s1]=tempArray1[m]; 263 m++; 264 s1++; 265 } 266 else{ 267 newgeneration[p2][s2]=tempArray1[m]; 268 m++; 269 s2++; 270 } 271 272 273 for(i=0;i<s1;i++) 274 if(tempArray2[n]==newgeneration[p2][i]) 275 break; 276 if(i==s1) 277 { 278 newgeneration[p1][s1]=tempArray2[n]; 279 n++; 280 s1++; 281 } 282 else{ 283 newgeneration[p2][s2]=tempArray2[n]; 284 n++; 285 s2++; 286 } 287 } 288 289 } 290 291 //變異操作 292 void mutation(int **newgeneration,int p1) 293 { 294 int rand1,rand2; 295 int temp; 296 int i; 297 298 srand(time(NULL)); 299 300 for(i=0;i<count;i++) 301 { 302 303 rand1=(int)(((double)rand()/RAND_MAX)*cityNum); 304 rand2=(int)(((double)rand()/RAND_MAX)*cityNum); 305 while(rand1==rand2) 306 { 307 rand2=(int)(((double)rand()/RAND_MAX)*cityNum); 308 } 309 310 temp=newgeneration[p1][rand1]; 311 newgeneration[p1][rand1]=newgeneration[p1][rand2]; 312 newgeneration[p1][rand2]=temp; 313 } 314 315 } 316 317 //對一代群體進行交叉變異操作 318 void CrossAndMutation(int **newgeneration,int cityNum) 319 { 320 float rand1,rand2; 321 int k; 322 for(k=0;k<cityNum;k=k+2) 323 { 324 srand(time(NULL)); 325 rand1=(float)rand()/RAND_MAX; 326 327 if(rand1>pc) 328 { 329 APCrossover(newgeneration,k,k+1,cityNum); 330 } 331 else 332 { 333 rand2=(float)rand()/RAND_MAX; 334 if(rand2>pm) 335 { 336 mutation(newgeneration,k); 337 } 338 rand2=(float)rand()/RAND_MAX; 339 if(rand2>pm) 340 { 341 mutation(newgeneration,k+1); 342 } 343 344 } 345 } 346 347 } 348 349 int main() 350 { 351 int **a; 352 int **oldGeneration; 353 int **newGeneration; 354 int i,j; 355 356 MAX_Gene=20; 357 cityNum=10; 358 scale=10; 359 bestlength=888888; 360 pc=0.6; 361 pm=0.5; 362 count=4; 363 int m; 364 365 366 a=creatArray2(scale,cityNum); 367 pathlength=creatArray1(scale); 368 cumPropa=creatArraydoub(scale); 369 bestpath=creatArray1(cityNum); 370 371 readfile(a,scale,cityNum); 372 373 oldGeneration=creatArray2(scale,cityNum); 374 newGeneration=creatArray2(scale,cityNum); 375 376 Initialize(newGeneration,scale,cityNum); 377 378 for(m=0;m<MAX_Gene;m++) 379 { 380 NcopyO(newGeneration,oldGeneration); 381 cumDistance(oldGeneration,a,pathlength,scale,cityNum); 382 cumulatePro(oldGeneration,cumPropa,pathlength,scale); 383 rouletteAlgo(oldGeneration,cumPropa,newGeneration,scale); 384 CrossAndMutation(newGeneration,cityNum); 385 386 } 387 388 printf("The best path is :\n"); 389 390 391 for(i=0;i<cityNum;i++) 392 { 393 printf("%d ",bestpath[i]); 394 } 395 printf("\n"); 396 397 printf("The minmum length is %d\n",bestlength); 398 399 return 0; 400 }
遺傳算法只能得到問題的近似最優解,而且對不同的問題,該算法的性能也不一樣。一般要求結合問題的一些特點和屬性或者與其他的演化算法相結合,例如蟻群算法,粒子群算法,模擬退火法等。