模擬退火 \(Simulated\) \(Annealing\) , 簡稱 \(SA\) ,最先在 \(1953\) 年由 \(N. Metropolis\) 提出,後經優化獲得如今普遍應用的算法,應用在不少領域當中。算法
本文題目連接函數
模擬退火是隨機化搜索的一種,若隨機化搜索寫得好,則能夠實現高效率和答案的正確率高(雖然說不是 \(100\%\) )。不少時候在想不出解決辦法,或方法的時間複雜度出現極大狀況時,可以使用模擬退火。所說是有較大概率正確,但仍是有疏漏,那麼能夠屢次試驗來更加接近準確地求出這個值(仍是要看運氣)。優化
模擬退火,顧名思義,是模擬工業上固體降溫的過程。先將固體加溫到必定的溫度後,在按照適當的溫度進行冷卻,冷卻到改物體想要達到的狀態。溫度下降地越慢,則該物體的質量約高,由於分子在因熱加速運動中找到了更加合適的位置。當溫度逐漸下降,分子運動減緩,達成目的。ui
那麼這一現象被科學家與計算機算法所聯繫起來,就成了如今的模擬退火。spa
網上的一張圖,模擬退火可視化:
code
模擬退火有幾個很關鍵的參數,這幾個參數決定了模擬退火的優劣。blog
#define Seed 19260817 #define TempLess 0.9975 #define TempHigh 2879.0 #define TempLow 1e-12
來看看降火的主體部分。圖片
void SA() { double temp = TempHigh;//初始化溫度 定義初始狀態; while(temp > TempLow) {//打到降溫條件 double nowans = Get_Ans(當前狀態);//更新最優解 double diff = nowans - ans;//與當前答案的差值 if(diff > 0) {//比當前答案更優 轉移狀態; ans = nowans;//更新答案 } else if(exp(-diff / temp) * RAND_MAX < rand()) { //接受這個解,爲何這樣寫請見例題部分 轉移狀態; } temp *= TempLess;//降溫 } }
模擬退火查詢的是多峯函數的最值。ci
如下曲線是解析式爲 \(y=0.05x^3-0.5x^2\) 的函數的圖像:
先來考慮貪心的作法:
當到達點 \(A\) 時,程序會選擇更高的一個點,那麼會從 \(A\) 點到達 \(B\) 點,而再從 \(B\) 點俯瞰,看到了點 \(C\) ,因爲 \(C\) 的縱座標比 \(B\) 小,因此點 \(B\) 不會到達點 \(C\) 。換句話說,該程序 \(100\%\) 不會接受點 \(C\) ,進而更不會到達點 \(D\) 。不難發現,這時候找到的局部最優解並非全局最優解。get
而模擬退火再次作出了改進。假設初始位置在點 \(A\) ,則會基於 \(A\) 作出左右擺動,通過數次擺動後到達 \(B\) 。再進一步擺動,假設擺動到了 \(C\) 點,可是 \(C\) 的縱座標比 \(B\) 小,會以必定概率以 \(C\) 的縱座標來接受 \(C\) 。進而在以一樣的方式擺動到點 \(D\) ,找到更高點。
因爲是該算法隨機性較高,因此多跑幾遍該函數。
下面結合一道例題更加深刻地探究,題目連接已在上文給出。
有 \(n\) 個圓, \(m\) 個點,請求出一個半徑不超過 \(r\) 的圓,使得與這 \(n\) 個圓沒有交集,且可以覆蓋的點最大。
此題的答案圓的圓心並不知足是整數,且由橫縱座標兩個值來影響,並不具備規律。這樣的問題一般使用模擬退火來解決。
void SA() { double temp = TempHigh;//初始化溫度 定義初始狀態; while(temp > TempLow) {//打到降溫條件 double nowans = Get_Ans(當前狀態);//更新最優解 double diff = nowans - ans;//與當前答案的差值 if(diff > 0) {//比當前答案更優 轉移狀態; ans = nowans;//更新答案 } else if(exp(-diff / temp) * RAND_MAX < rand()) {//接受這個解 轉移狀態; } temp *= TempLess;//降溫 } }
如上,初始狀態包含了橫座標和縱座標,爲了提升正確率與效率,設爲全部點的橫縱座標的平均值。
\(GetAns\) 函數也很簡單,先肯定半徑,半徑爲這個點到各個圓的切線的距離的最小值,即兩個圓心的距離減去當前枚舉到的這個圓的半徑。後枚舉每一個點,若這個點被覆蓋則 \(res++\) ,最後返回 \(res\) 。
double Get_Ans(double x, double y) { double res = 0; double rkill = r; for(int i = 1; i <= n; i++)//枚舉圓 rkill = Min(rkill, Dist_Cartesian(XC(i), YC(i), x, y) - RC(i)); for(int i = 1; i <= m; i++)//枚舉點 if(Dist_Cartesian(XE(i), YE(i), x, y) <= rkill) res += 1.0; return res; }
有了 \(GetAns\) 函數,主題部分也很快能出來。
void SA() { double temp = TempHigh, ansx = initx, ansy = inity;//降溫前初始化 while(temp > TempLow) { double nowx = ansx + ((rand() << 1) - RAND_MAX) * temp; double nowy = ansy + ((rand() << 1) - RAND_MAX) * temp; double nowans = Get_Ans(nowx, nowy); double diff = nowans - ans; if(diff > 0) { initx = nowx; inity = nowy; ansx = nowx; ansy = nowy; ans = nowans; } else if(exp(-diff / temp) * RAND_MAX < rand()) { ansx = nowx; ansy = nowy; } temp *= TempLess; } }
首先來看這段代碼
double nowx = ansx + ((rand() << 1) - RAND_MAX) * temp; double nowy = ansy + ((rand() << 1) - RAND_MAX) * temp;
由答案左右擺動,生成新的當前狀態 \(nowx\) 和 \(nowy\) ,擺動幅度是隨機的,應該是由分子作無規則運動而來。乘上 \(temp\) 當前溫度是由分子在越熱的環境中,運動得越快而得來。
緊接着兩行就是求出當前狀態的答案,在求出它與當前最優解的差值。
第一個 \(if\) 是當前這個局部解大於當前最優解,則用當前最優的局部解來更新最優解。
重點是下一個 \(if\) ,這行代碼就是它與貪心的不一樣,以必定概率接受這個解,在用它更新當前狀態,進行左右擺動,從而找到局部更優解,更加接近總體最優解。其條件的優越性由 \(Metropolis\) 接受準則給出。也就是 \(else\) \(if\) 中的條件:
exp(-diff / temp) * RAND_MAX < rand()
思路整理完了,此題並無多少思惟難度,可是須要對上述幾個參數進行調整,能夠多總結一些正確率大的參數,以備下次使用
#include <cmath> #include <cstdio> #include <cstdlib> #define Min(a, b) ((a) < (b) ? (a) : (b)) #define Seed 19260817//隨機種子 #define TempLess 0.9975//溫度變化率 #define TempHigh 2879.0//初始溫度 #define TempLow 1e-12//目標溫度 void Quick_Read(double &N) {//double快速讀入 N = 0.0; double now, wei = 0.1; bool op = false; char c = getchar(); while(c < '0' || c > '9') { if(c == '-') op = -1; c = getchar(); } while(c >= '0' && c <= '9') { N = N * 10.0 + (c ^ 48) * 1.0; c = getchar(); } if(c == '.') { c = getchar(); while(c >= '0' && c <= '9') { N += (c ^ 48) * wei; wei /= 10.0; c = getchar(); } } if(op) N = -N; } const int MAXN = 15; const int MAXM = 1e3 + 5; struct Circle {//題目中的圓 double Abscissa_C, Ordinate_C, Radius_C; #define XC(x) buildings[x].Abscissa_C #define YC(x) buildings[x].Ordinate_C #define RC(x) buildings[x].Radius_C }; Circle buildings[MAXN]; struct Enemy {//題目中的點 double Abscissa_E, Ordinate_E; #define XE(x) foe[x].Abscissa_E #define YE(x) foe[x].Ordinate_E }; Enemy foe[MAXM]; double n, m, r; double initx, inity;//記錄答案的橫縱座標 double ans;//答案 double Dist_Cartesian(double XA, double YA, double XB, double YB) {//兩點間距離公式 double frontx = (XA - XB) * (XA - XB); double fronty = (YA - YB) * (YA - YB); double dist = sqrt(frontx + fronty); return dist; } double Get_Ans(double x, double y) {//找到當前狀態的答案 double res = 0; double rkill = r; for(int i = 1; i <= n; i++)//求出最大半徑 rkill = Min(rkill, Dist_Cartesian(XC(i), YC(i), x, y) - RC(i)); for(int i = 1; i <= m; i++)//求出被圓覆蓋的點 if(Dist_Cartesian(XE(i), YE(i), x, y) <= rkill) res += 1.0; return res; } void SA() { double temp = TempHigh, ansx = initx, ansy = inity;//初始化 while(temp > TempLow) {//降溫 double nowx = ansx + ((rand() << 1) - RAND_MAX) * temp;//當前狀態x double nowy = ansy + ((rand() << 1) - RAND_MAX) * temp;//當前狀態y double nowans = Get_Ans(nowx, nowy);//當前局部答案 double diff = nowans - ans;//當前答案與最優解的差值 if(diff > 0) {//比當前最優解更優則更新最優解 initx = nowx; inity = nowy; ansx = nowx; ansy = nowy; ans = nowans; } else if(exp(-diff / temp) * RAND_MAX < rand()) { //按照Metropolis接受準則接受改狀態 ansx = nowx; ansy = nowy; } temp *= TempLess;//降溫 } } void Cool_Down() { int frequ = 6; while(frequ--)//隨機化算法儘可能多跑幾回 SA(); } void Make_Seed() {//生成隨機種子 srand(Seed); } void Read() {//輸入 Quick_Read(n); Quick_Read(m); Quick_Read(r); for(int i = 1; i <= n; i++) { Quick_Read(XC(i)); Quick_Read(YC(i)); Quick_Read(RC(i)); } for(int i = 1; i <= m; i++) { Quick_Read(XE(i)); Quick_Read(YE(i)); initx += XE(i); inity += YE(i); } initx /= m;//以平均值開始提升效率與準確率 inity /= m; } void Write() {//輸出 printf("%.0lf", ans); } int main() { Make_Seed(); Read(); Cool_Down(); Write(); return 0; }