模擬退火筆記(詳細)
彆著急,乾貨在最後面!!!(本文用c++實現)
很多人都學過貪心,但是貪心在一些情況並不適用,比如:
已知我們從黃色出發,找最小值。
貪心策略當然是一直往函式大小減小的地方偏移——但是,萬一不是單峰呢?我們會陷入如圖的藍色中無法自拔。
肯能你會想到:隨機找一個點出發,然後貪心找最小值?多隨機幾遍,然後求全域性最小值?
你會發現複雜度暴增!!!!!!!!——所以如何處理這種問題呢?
模擬退火:啊,對對對~
是的!模擬退火就是一種類似於隨機化貪心的一個演算法,在OI界也小有名氣(冥器)!(如題[NOIP2021] 方差 )
原理圖:
如圖:在物理應用中分子排布可能是紊亂的,如果我們將它升溫然後緩慢降溫,就可以生成完美的晶形!
所以我們立刻(啊,對對對~)能設定模擬退火的引數:
1.初始溫度 T (1000-7000)
2.末尾溫度 P(1e-5~1e-15)
3.降溫係數 K (0.91~0.9975)
4.狀態空間(被降溫物體) S
5.當前能量 E ( new )
6.全域性能量 E ( old )
一:Metropolis準則- 以概率接受新狀態:
這就是物理(化學)方面類似的推論——一定概率的更新。
什麼意思呢?
我們已知:當前能量 E ( new ) , 全域性能量 E ( old ),那麼我們的目標是什麼,不就是減少目前的能量嗎?
所以:噹噹前能量少於全域性能量(即更新前的能量),那麼我們有概率為 1 的更新概率;
噹噹前能量大於全域性能量(即更新前的能量),那麼我們有概率為 exp( - (E( new )-E( old ))/T) 的更新概率 ( T為當前溫度) ;
注意:有時候也不一定以以上方式更新,這只是比較妥的做法,概率方面是可以自己定的,但是一定以當前能量與全域性能量的關係來設定的。(除非直接暴力的隨機演算法)
二:
那麼怎麼生成新的當前溫度呢?,以生成小數為例:
當前將更新溫度=全域性溫度+(rand()*2-RAND_MAX)*t; if(不在狀態空間內){ 當前將更新溫度=fmod(當前將更新溫度,狀態空間大小) }
即:在當前狀態的鄰域結構內以一定概率方式(均勻分佈、正態分佈、指數分佈等)產生。
三:溫度更新函式
若固定每一溫度,演算法均計算至平穩分佈,然後下降溫度,則稱為時齊演算法;
若無需各溫度下演算法均達到平穩分佈,但溫度需按一定速率下降,則稱為非時齊演算法。
本人用的:
T*=K;
四:內迴圈終止準則
本人使用的:
(t>1e-15)//可以改大一點
其他常用方法:
(1)設定終止溫度的閾值。
(2)設定外迴圈迭代次數。
(3)演算法搜尋到的最優值連續若干步保持不變。
(4)概率分析方法。
五:實現流程圖:
注意:以下是我自己總結的退火口訣:
初始溫度小心設(1000-3000),又粗又大wa一臉
多次sa更保險,忘了卡時直接T[if((double)clock()/CLOCKS_PER_SEC>=0.993)]
退火係數大膽設,不過0.9975會很厄
全域性、狀態不一樣,全域性必須菊部優
百年騙分一場空,不開srand見祖宗
退火需謹慎,退火不規範,靈封兩行淚
然後是[NOIP2021]方差的實現(玄學萬歲):
#include <bits/stdc++.h> using namespace std; #define LL long long const int N=1e5+10; const double dw=0.9975; int a[N],n,c[N]; long long ans; bool cmp(int a,int b){ return a>b; } LL en(){ LL em=0; LL ranss=0; for(int i=2;i<=n;i++){ a[i]=a[i-1]+c[i]; } for(int i=1;i<=n;i++){ em+=(long long)a[i]*a[i]; }em=(long long)em*n; for(int i=1;i<=n;i++){ ranss=(long long)ranss+a[i]; }ranss=(long long)ranss*ranss; return (long long)(em-ranss); } void sa(){ double t=1000; while(t>1e-15){ if((double)clock()/CLOCKS_PER_SEC>=0.993){ cout<<ans; exit(0); } int x=rand()%(n-1)+2,y=rand()%(n-1)+2; while(x==y)x=rand()%(n-1)+2; swap(c[x],c[y]); LL m=en(),dt=ans-m; if(dt>0){ ans=m; }else if((double)rand()>=(double)RAND_MAX*(double)exp((double)dt/t)){ swap(c[x],c[y]); } t*=dw; } } int main(){ srand((unsigned)time(0)); cin>>n; for(int i=1;i<=n;i++){ scanf("%d",&a[i]); c[i]=a[i]-a[i-1]; } sort(c+2,c+n/2+1,cmp); sort(c+n/2+1,c+n+1); ans=en(); while(1)sa(); }