別着急,干貨在最后面!!!
(本文用c++實現,可以在評論區討論,后面還有情況的話還會更新,有問題歡迎指正哦~)
可以在右上角看目錄,左下角點歌哦(不行的話刷新一下就好了~)
本文章也介紹了模擬退火的使用情景,以免誤入歧途(本蒟蒻就是)。
很多人都學過貪心,但是貪心在一些情況並不適用,比如:
已知我們從黃色出發,找最小值。
貪心策略當然是一直往函數大小減小的地方偏移——但是,萬一不是單谷呢?我們會陷入如圖的藍色中無法自拔。
肯能你會想到:隨機找一個點出發,然后貪心找最小值?多隨機幾遍,然后求全局最小值?
你會發現復雜度暴增!!!!!!!!——所以如何處理這種問題呢?
模擬退火:啊,對對對~
是的!模擬退火就是一種類似於隨機化貪心的一個算法,在OI界也小有名氣(冥器)!(如題[NOIP2021] 方差 )
原理圖:
如圖:在物理應用中分子排布可能是紊亂的,如果我們將它升溫然后緩慢降溫,就可以生成完美的晶形!
而對於我們求解的:
怎么形象描述它呢?
一個有自己一定卡路里的人爬山,想翻山找遠方的草葯給自己心愛的妻子,但是他並不知道山的那頭是什么。所以他會在卡路里多的時候盡量去遠方探險,但是每次會花費卡路里以至於他后面不能翻過太高的山丘,而且他的背包蠻大的,裝填着無數愛的草葯而芳香四溢。
所以我們立刻(啊,對對對~)能設定模擬退火的參數:
1.初始溫度 T (1000-7000)
2.末尾溫度 P(1e-6~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為當前溫度)
[exp(x)函數:e的x次方的函數 如 exp(1)表示e的1次方=e=2.718281828… exp(0)表示e的0次方=1 exp(2)表示e的平方=7.3890561… e是一個常數,等於2.718281828…];
注意:有時候也不一定以以上方式更新,這只是比較妥的做法,概率方面是可以自己定的,但是一定以當前能量與全局能量的關系來設定的。(除非直接暴力的隨機算法)
二:生成新溫度
那么怎么生成新的當前溫度呢?,以生成小數為例:
當前將更新溫度=全局溫度+(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見祖宗
退火需謹慎,退火不規范,靈封兩行淚
八:使用條件:
我們可以看下這道題:https://www.luogu.com.cn/problem/P6879
第一次做看到最優解我直接退火了,要不是捆綁數據還以為自己正確了。
代碼:
#include <bits/stdc++.h> using namespace std; const int N=500; const double dw=0.9975; int resx,n,ans,l,T[N]; long long a[N]; bool st[N]; int em(int x){ memset(st,false,sizeof st); int res=0,pd=0; pd=min(abs(x-0),abs(x-l)); for(int i=1;i<=n*2;i++){ if(abs(a[i]-x)+pd<=T[i]&&!st[i]){ res++;st[i%(2*n)]=st[(i+n)%(2*n)]=true; } } return res+(rand()-RAND_MAX+1)%2; } void sa(){ double t=1500; while(t>1e-15){ int x=abs(resx+rand()*2-RAND_MAX+1)%l; int e=em(x),dt=ans-e; if(dt<0){ ans=e;resx=x; }else if(exp((double)(-dt/t))*RAND_MAX<rand()){ resx=x; } t*=dw; } } int main(){ srand((unsigned)time(0)); cin>>n>>l; for(int i=1;i<=n;i++){ scanf("%lld",&a[i]); a[i+n]=a[i]+l; } for(int i=1;i<=n;i++){ scanf("%d",&T[i]); T[i+n]=T[i]; } l*=2; ans=em(0); sa();sa();sa();sa();sa();sa();sa(); cout<<ans; }
發現可以A掉一部分?其實是不對滴——計算當前物品能量的函數寫的並不是正確的。
因為當你到了圓上的一點后,發現你仍然需要下一步決策走向最優解,而並不是直接可以計算出來可以得到的貢獻值。
這不禁讓我反思——退火可以解DP題嗎?
我們可以觀察一下這道題:大的最優解是由更小的最優解轉移過來的,就像一個樹形結構,由子節點向父節點轉移,像這樣的題是不可以用退火的。
但是比如01背包, [NOIP2021]方差 和等類貪心題目是可以做的,因為你可以通過概率水掉局部最優解對全局最優解的不可轉移。
所以,每次做題要看退火的當前能量計算復不復雜,有沒有子問題的限制!
然后是[NOIP2021]方差的實現(玄學萬歲):
1 #include <bits/stdc++.h> 2 using namespace std; 3 #define LL long long 4 const int N=1e5+10; 5 const double dw=0.9975; 6 int a[N],n,c[N]; 7 long long ans; 8 bool cmp(int a,int b){ 9 return a>b; 10 } 11 LL en(){ 12 LL em=0; 13 LL ranss=0; 14 for(int i=2;i<=n;i++){ 15 a[i]=a[i-1]+c[i]; 16 } 17 for(int i=1;i<=n;i++){ 18 em+=(long long)a[i]*a[i]; 19 }em=(long long)em*n; 20 for(int i=1;i<=n;i++){ 21 ranss=(long long)ranss+a[i]; 22 }ranss=(long long)ranss*ranss; 23 return (long long)(em-ranss); 24 } 25 void sa(){ 26 double t=1000; 27 while(t>1e-15){ 28 if((double)clock()/CLOCKS_PER_SEC>=0.993){ 29 cout<<ans; 30 exit(0); 31 } 32 int x=rand()%(n-1)+2,y=rand()%(n-1)+2; 33 while(x==y)x=rand()%(n-1)+2; 34 swap(c[x],c[y]); 35 LL m=en(),dt=ans-m; 36 if(dt>0){ 37 ans=m; 38 }else if((double)rand()>=(double)RAND_MAX*(double)exp((double)dt/t)){ 39 swap(c[x],c[y]); 40 } 41 t*=dw; 42 } 43 } 44 int main(){ 45 srand((unsigned)time(0)); 46 cin>>n; 47 for(int i=1;i<=n;i++){ 48 scanf("%d",&a[i]); 49 c[i]=a[i]-a[i-1]; 50 } 51 sort(c+2,c+n/2+1,cmp); 52 sort(c+n/2+1,c+n+1); 53 ans=en(); 54 while(1)sa(); 55 }
附贈導論:騙分。