「筆記」模擬退火
寫在前面
感謝 caq 的傾情講解
模擬退火是個隨機化演算法,正確性有一定保證,但如果你想我一樣臉黑的話......
實測模擬退火做多了 rp 會掉
正文
簡介
模擬退火是一種隨機化演算法,當一個問題的方案數量極大(甚至是無窮的)而且不是一個單峰函式時,我們常使用模擬退火求解。- Oi-wiki
什麼是退火?
退火是一種金屬熱處理工藝,指的是將金屬緩慢加熱到一定溫度,保持足夠時間,然後以適宜速度冷卻。目的是降低硬度,改善切削加工性;消除殘餘應力,穩定尺寸,減少變形與裂紋傾向;細化晶粒,調整組織,消除組織缺陷。準確的說,退火是一種對材料的熱處理工藝,包括金屬材料、非金屬材料。而且新材料的退火目的也與傳統金屬退火存在異同。---百度百科
扯遠了。
這個演算法就是在溫度不斷降低的過程中,不斷地從當前位置尋找別的位置進行計算,溫度越低,也就是它的動能越小時,位置就會變化的越小,最後逐漸停留在最優解(或者附近)
演算法流程
每次隨機一個新的狀態,如果狀態更優就更新答案,否則以一定概率接受這個狀態。
Metropolis準則
以求最小值為例。
-
如果 \(\Delta E < 0\),說明當前解更優,直接更新即可
-
否則,如果
就接受這個狀態。
- 否則 直接跳過。
為什麼?
第一步因為是最優解所以一定選擇更新答案
第二步後邊的是一個隨機值我們暫且不論。
考慮整個退火過程,
假設溫度 \(T\)
假設 \(\Delta E\) 不變,隨著溫度的下降,求解的範圍也趨於穩定,\(T\) 越小,左項得值也越小,接受的概率也越小
扔一張圖可能會更好理解:
聽上去很扯 ,但它還是有一定的正確性的。
SA 函式
通常降溫係數 \(d\) 是一個很接近 \(1\) 的數,終止溫度 \(T_0\) 是一個很接近 \(0\) 的數
這裡給一個虛擬碼:
const double lim = ... // 溫度最小值,通常為 1e-10 左右 const double d = ... // 變化係數,通常為 0.996 左右 void SA() { double T = ... // 初始溫度,通常為 2021 左右 while(T > lim) { ... // 獲取一個隨機的位置 now = calc(); // 計算當前位置的答案 del = now - ans; // 計算 變化量 if(del < 0) { // 以最小值為例 ans = now; // 更新答案 ... // 更新答案和中間量的狀態 } else if(exp(-del/T) > (double)rand()/RAND_MAX) { ... // 一定概率選擇當前當前狀態 } T *= d; // 降溫 } }
計算函式 calc
依據題目而定,這裡不給出
一些技巧
如果想要隨機一個無限大平面內的一個點,可以這樣:
double nowx = limx + ((rand() << 1) - RAND_MAX) * T;
double nowy = limy + ((rand() << 1) - RAND_MAX) * T;
其中 nowx,nowy
是我們隨機的位置, limx, limy
是我們一箇中間狀態的位置(注意不是答案的位置),
後面的那一坨剛好對應著溫度越小變化越小的實際情況。
我們有時為了使得到的解更有質量,會在模擬退火結束後,以當前溫度在得到的解附近多次隨機狀態,嘗試得到更優的解(其過程與模擬退火相似)。
模擬退火是個隨機的演算法,執行次數越多獲得的解越有可能更優,所以我們可以執行多遍 SA 函式。至於如何控制時間?
while((double)clock()/CLOCKS_PER_SEC < 0.90) SA();
上面這個程式碼控制時間在 \(0.90s\) 左右,如果時間限制為 \(1s\),而每次 SA 函式執行時間略長時,就要小心可能會 \(\text{T}\) 掉了。
如果一個程式碼不行,就考慮換個種子吧。
srand(...);
為了獲得更精確的解,也可以把 \(d\) 和 \(T_0\) 調的更精準一點
const double d = 0.996 -> 0.99996;
const double lim = 1e-10 -> 1e-15;
還有,隨機亂搞一些 初溫,終溫,降溫係數 也是可以的。
例題
咕咕咕。。。