更多精彩盡在微信公眾號【程序猿聲】

01 什么是旅行商問題(TSP)?
TSP問題(Traveling Salesman Problem,旅行商問題),由威廉哈密頓爵士和英國數學家克克曼T.P.Kirkman於19世紀初提出。問題描述如下:
有若干個城市,任何兩個城市之間的距離都是確定的,現要求一旅行商從某城市出發必須經過每一個城市且只在一個城市逗留一次,最后回到出發的城市,問如何事先確定一條最短的線路已保證其旅行的費用最少?
如下圖所示:

02 模擬退火算法(Simulate Annealing Arithmetic,SAA)
2.1 什么是模擬退火算法(簡介)?
模擬退火算法(Simulate Annealing Arithmetic,SAA)是一種通用概率演算法,用來在一個大的搜尋空間內找尋命題的最優解。它是基於Monte-Carlo迭代求解策略的一種隨機尋優算法。
模擬退火算法是S.Kirkpatrick, C.D.Gelatt和M.P.Vecchi等人在1983年發明的,1985年,V.Černý也獨立發明了此演算法。模擬退火算法是解決TSP問題的有效方法之一。
2.2 模擬退火算法的來源
模擬退火算法來源於固體退火原理。
物理退火: 將材料加熱后再經特定速率冷卻,目的是增大晶粒的體積,並且減少晶格中的缺陷。材料中的原子原來會停留在使內能有局部最小值的位置,加熱使能量變大,原子會離開原來位置,而隨機在其他位置中移動。退火冷卻時速度較慢,使得原子有較多可能可以找到內能比原先更低的位置。
模擬退火: 其原理也和固體退火的原理近似。模擬退火算法從某一較高初溫出發,伴隨溫度參數的不斷下降,結合概率突跳特性在解空間中隨機尋找目標函數的全局最優解,即在局部最優解能概率性地跳出並最終趨於全局最優。
2.3 模擬退火算法思想
在介紹模擬退火算法之前,有必要給大家科普一下爬山算法 (Hill Climbing)。
2.3.1 爬山算法
爬山算法是一種簡單的貪心搜索算法,該算法每次從當前解的臨近解空間中選擇一個最優解作為當前解,直到達到一個局部最優解。這種算法思想很單純,但是也存在一個很大的缺陷。在搜索選擇的過程中有可能會陷入局部最優解,而這個局部最優解不一定是全局最優解。比如下面這個問題:

假設A是當前解,爬山算法往前繼續搜索,當搜索到B這個局部最優解時就會停止搜索了。因為此時在B點無論是往哪邊走都不會得到更優的解了。但是,聰明的同學已經發現了,全局最優解在C點。
2.3.2 模擬退火算法
爬山法是完完全全的貪心法,這種貪心是很鼠目寸光的,只把眼光放在局部最優解上,因此只能搜索到局部的最優值。模擬退火其實也是一種貪心算法,只不過與爬山法不同的是,模擬退火算法在搜索過程引入了隨機因素。模擬退火算法以一定的概率來接受一個比當前解要差的解,因此有可能會跳出這個局部的最優解,達到全局的最優解。從上圖來說,模擬退火算法在搜索到局部最優解B后,會以一定的概率接受向右的移動。也許經過幾次這樣的不是局部最優的移動后會到達BC之間的峰點D,這樣一來便跳出了局部最優解B,繼續往右移動就有可能獲得全局最優解C。如下圖:

關於普通Greedy算法與模擬退火,這里也有一個有趣的比喻:
普通Greedy算法:兔子朝着比現在低的地方跳去。它找到了不遠處的最低的山谷。但是這座山谷不一定最低的。
模擬退火:兔子喝醉了。它隨機地跳了很長時間。這期間,它可能走向低處,也可能踏入平地。但是,它漸漸清醒了並朝最低的方向跳去。
如此一來,大家對模擬退火算法有了一定的認識,但是這還是不夠的。對比上面兩種算法,對於模擬退火算法我們提到了一個很important的概念--一定的概率,關於這個一定的概率是如何計算的。這里還是參考了固體的物理退火過程。
根據熱力學的原理,在溫度為T時,出現能量差為dE的降溫的概率為P(dE),表示為:
P(dE) = exp( dE/(kT) )
其中k是一個常數,exp表示自然指數,且dE<0(溫度總是降低的)。這條公式指明了:
1) 溫度越高,出現一次能量差為dE的降溫的概率就越大;
2) 溫度越低,則出現降溫的概率就越小。又由於dE總是小於0(不然怎么叫退火),因此dE/kT < 0 ,exp(dE/kT)取值是(0,1),那么P(dE)的函數取值范圍是(0,1) 。
隨着溫度T的降低,P(dE)會逐漸降低。我們將一次向較差解的移動看做一次溫度跳變過程,我們以概率P(dE)來接受這樣的移動。也就是說,在用固體退火模擬組合優化問題,將內能E模擬為目標函數值 f,溫度T演化成控制參數 t,即得到解組合優化問題的模擬退火演算法:由初始解 i 和控制參數初值 t 開始,對當前解重復“產生新解→計算目標函數差→接受或丟棄”的迭代,並逐步衰減 t 值,算法終止時的當前解即為所得近似最優解。
因此我們歸結起來就是以下幾點:
1) 若f( Y(i+1) ) <= f( Y(i) ) (即移動后得到更優解),則總是接受該移動;
2) 若f( Y(i+1) ) > f( Y(i) ) (即移動后的解比當前解要差),則以一定的概率接受移動,而且這個概率隨着時間推移逐漸降低(逐漸降低才能趨向穩定)相當於上圖中,從B移向BC之間的小波峰D時,每次右移(即接受一個更糟糕值)的概率在逐漸降低。如果這個坡特別長,那么很有可能最終我們並不會翻過這個坡。如果它不太長,這很有可能會翻過它,這取決於衰減 t 值的設定。
2.4 模擬退火算法偽代碼
相信通過上面的講解,大家已經對模擬退火算法認識得差不多了。下面我們來看看它的偽代碼是怎么實現的。

03 使用模擬退火算法解決旅行商問題
旅行商問題屬於所謂的NP完全問題。精確的解決TSP只能通過窮舉所有的路徑組合,其時間復雜度是O(N!) 。而使用模擬退火算法則可以較快速算法一條近似的最優路徑。大體的思路如下:
- 產生一條新的遍歷路徑P(i+1),計算路徑P(i+1)的長度L( P(i+1) )
- 若L(P(i+1)) < L(P(i)),則接受P(i+1)為新的路徑,否則以模擬退火的那個概率接受P(i+1) ,然后降溫
- 重復步驟1,2直到滿足退出條件
好了多說無益,下面大家一起看代碼吧。基於中國31個城市跑的。
/*
* 使用模擬退火算法(SA)求解TSP問題(以中國TSP問題為例)
* 參考自《Matlab 智能算法30個案例分析》
*/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<time.h>
#include<math.h>
#define T0 50000.0 // 初始溫度
#define T_end (1e-8)
#define q 0.98 // 退火系數
#define L 1000 // 每個溫度時的迭代次數,即鏈長
#define N 31 // 城市數量
int city_list[N]; // 用於存放一個解
// 中國31個城市坐標
double city_pos[N][2] =
{
{1304,2312},{3639,1315},{4177,2244},{3712,1399},
{3488,1535},{3326,1556},{3238,1229},{4196,1004},
{4312,790},{4386,570},{3007,1970},{2562,1756},
{2788,1491},{2381,1676},{1332,695},
{3715,1678},{3918,2179},{4061,2370},
{3780,2212},{3676,2578},{4029,2838},
{4263,2931},{3429,1908},{3507,2367},
{3394,2643},{3439,3201},{2935,3240},
{3140,3550},{2545,2357},{2778,2826},
{2370,2975}};
//函數聲明
double distance(double *,double *); // 計算兩個城市距離
double path_len(int *); // 計算路徑長度
void init(); //初始化函數
void create_new(); // 產生新解
// 距離函數
double distance(double * city1,double * city2)
{
double x1 = *city1;
double y1 = *(city1+1);
double x2 = *(city2);
double y2 = *(city2+1);
double dis = sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2));
return dis;
}
// 計算路徑長度
double path_len(int * arr)
{
double path = 0; // 初始化路徑長度
int index = *arr; // 定位到第一個數字(城市序號)
for(int i=0;i<N-1;i++)
{
int index1 = *(arr+i);
int index2 = *(arr+i+1);
double dis = distance(city_pos[index1-1],
city_pos[index2-1]);
path += dis;
}
int last_index = *(arr+N-1); // 最后一個城市序號
int first_index = *arr; // 第一個城市序號
double last_dis = distance(city_pos[last_index-1],
city_pos[first_index-1]);
path = path + last_dis;
return path; // 返回總的路徑長度
}
// 初始化函數
void init()
{
for(int i=0;i<N;i++)
city_list[i] = i+1; // 初始化一個解
}
// 產生一個新解
// 此處采用隨機交換兩個位置的方式產生新的解
void create_new()
{
double r1 = ((double)rand())/(RAND_MAX+1.0);
double r2 = ((double)rand())/(RAND_MAX+1.0);
int pos1 = (int)(N*r1); //第一個交叉點的位置
int pos2 = (int)(N*r2);
int temp = city_list[pos1];
city_list[pos1] = city_list[pos2];
city_list[pos2] = temp; // 交換兩個點
}
// 主函數
int main(void)
{
srand((unsigned)time(NULL)); //初始化隨機數種子
time_t start,finish;
start = clock(); // 程序運行開始計時
double T;
int count = 0; // 記錄降溫次數
T = T0; //初始溫度
init(); //初始化一個解
int city_list_copy[N]; // 用於保存原始解
double f1,f2,df; //f1為初始解目標函數值,
//f2為新解目標函數值,df為二者差值
double r; // 0-1之間的隨機數,用來決定是否接受新解
while(T > T_end) // 當溫度低於結束溫度時,退火結束
{
for(int i=0;i<L;i++)
{
// 復制數組
memcpy(city_list_copy,city_list,N*sizeof(int));
create_new(); // 產生新解
f1 = path_len(city_list_copy);
f2 = path_len(city_list);
df = f2 - f1;
// 以下是Metropolis准則
if(df >= 0)
{
r = ((double)rand())/(RAND_MAX);
if(exp(-df/T) <= r) // 保留原來的解
{
memcpy(city_list,city_list_copy,N*sizeof(int));
}
}
}
T *= q; // 降溫
count++;
}
finish = clock(); // 退火過程結束
double duration = ((double)(finish-start))/CLOCKS_PER_SEC; // 計算時間
printf("模擬退火算法,初始溫度T0=%.2f,降溫系數q=%.2f,每個溫度迭代%d次,共降溫%d次,得到的TSP最優路徑為:\n",T0,q,L,count);
for(int i=0;i<N-1;i++) // 輸出最優路徑
{
printf("%d--->",city_list[i]);
}
printf("%d\n",city_list[N-1]);
double len = path_len(city_list); // 最優路徑長度
printf("最優路徑長度為:%lf\n",len);
printf("程序運行耗時:%lf秒.\n",duration);
return 0;
}
代碼運行結果:

04 小結
從上面的過程我們可以看出,模擬退火算法是一種隨機算法,它有一定的概率能求得全局最優解,但不一定。用模擬退火算法可以較快速地找出問題的最優近似解。它的關鍵之處還是在於允許一定的差解。不過,在小編不成熟的眼光看來,人生亦有相似之處。有時候可能放棄眼前短淺的利益,最終才可能獲得更好的未來。現在失去的,在未來會以另一種方式歸來。