模擬退火學習筆記
隨機化演算法沒有前途
簡介
模擬退火是一種隨機化(玄學)演算法。廣泛應用於各類求最值問題的騙分方法中。簡單來說,對於一個多峰函式,要求它的最值,就可以用模擬退火解決。
當然,如果直接隨機,正確率顯然很低。但是一般的實際問題中,函式即使沒有確定的單調性,在一個區間內函式的差值不會太大。於是就可以用到模擬退火來優化這個過程(可以理解為啟發式搜尋演算法)。
概念
溫度
在模擬退火中,最重要的概念是溫度,也可以理解為當前隨機化的區間。溫度是一個不斷降低的過程,那麼每一次隨機的區間就會不斷縮小,最終就會趨近於一個點。
初始溫度、終止溫度、衰減係數
初始溫度就是最開始的初始溫度(如初始區間為 \(10^5\)
當然,在正式比賽的時候,由於每次程式執行的時間也是隨機的,所以可以採用卡時的方法來決定隨機次數。
隨機選擇一個點
以求全域性最小值為例,在每次退火的過程中,會在當前的溫度範圍內隨機取一個點,計算該點的函式值,記新點的函式值與原函式值的差值為 \(\Delta E\)。接下來根據 \(\Delta E\) 的值進行分類討論:
1.\(\Delta E <0\)
2.\(\Delta E >0\),那麼以一定概率跳到新點上。因為原函式不是單峰的,如果只跳比當前點函式值小的點那麼可能會跳到一個區域性最小值上,而不是全域性最小值。此時跳過去的概率根據經驗值就可以設為 \(e^{\frac{-\Delta E}{T}}\),其中 \(T\) 表示當前的溫度。(根據指數函式的性質,這樣設定的概率一定在 \(0 \sim 1\) 之間,同時差值越低,跳過去的概率也就越高,反之越低。這樣也符合實際的過程)。
【模板】A Star not a Tree?
給定一個多邊形,求一個點,使得這個點到所有頂點的距離和最小。
資料範圍
\(n \leq 100\)
思路
通過各種證明可以得知,本題的函式影象是一個凸函式,可以用三分套三分求解。但是為了貫徹亂搞的精神學習模擬退火演算法,這裡就採用模擬退火解決本題。
可以直接按照前面提到的概念來寫。也可以通過多次模擬來降低出錯的概率。
code:
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<cstdlib>
#include<ctime>
using namespace std;
const int N=110;
struct node{double x,y;}p[N];
int n;double ans=1e15;
double dis(node a,node b){double dx=a.x-b.x,dy=a.y-b.y;return sqrt(dx*dx+dy*dy);}
double calc(node a){double res=0;for(int i=1;i<=n;i++) res+=dis(a,p[i]);ans=min(ans,res);return res;}//別忘了每次更新答案
double rand(double l,double r)//在l和r中隨機一個數
{
return 1.0*rand()/RAND_MAX*(r-l)+l;
}
void simulate_anneal()
{
node now=node{rand(0,10000),rand(0,10000)};
double S=1e4,T=1e-14,K=0.995;//起始溫度,終止溫度,衰減係數
for(double t=S;t>T;t*=K)
{
node tmp=node{rand(now.x-t,now.x+t),rand(now.y-t,now.y+t)};
double delta=calc(tmp)-calc(now);
if(exp(-delta/t)>rand(0,1)) now=tmp;
}
}
int main()
{
srand(time(0));int T;scanf("%d",&T);
while(T--)
{
ans=1e18;scanf("%d",&n);for(int i=1;i<=n;i++) scanf("%lf%lf",&p[i].x,&p[i].y);
for(int i=0;i<=10;i++) simulate_anneal();printf("%.0lf\n",ans);if(T) puts("");
}
return 0;
}
【應用】保齡球
本題的題目背景較為複雜,建議直接看原題背景。理解了題意以後,可以發現就是將原來的二元組重新排列使得總分最大。
思路
模擬退火也可以解決方案最優的問題,但是需要保證每一次操作不會使函式值發生太大變化。如本題中交換兩次遊戲之後對於答案的變化量不會特別大。那麼就可以按照一般的想法,每次隨機兩個下標交換,再根據交換前後的差值和隨機化來判斷是否可以進行交換。
code:
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<ctime>
#include<cstdlib>
using namespace std;
const int N=110;
int n,m,ans;
struct node{int a,b;}q[N];
int calc()
{
int res=0;
for(int i=1;i<=m;i++)
{
res+=q[i].a+q[i].b;if(i==m) continue;
if(q[i].a==10) res+=q[i+1].a+q[i+1].b;
else if(q[i].a+q[i].b==10) res+=q[i+1].a;
}
ans=max(ans,res);return res;
}
double rand(double l,double r){return 1.0*rand()/RAND_MAX*(r-l)+l;}
void simulate_anneal()
{
double S=1e5,T=1e-10,K=0.99;
for(double t=S;t>T;t*=K)
{
int x=rand()%m+1,y=rand()%m+1,delta=-calc();swap(q[x],q[y]);
if(n+(q[n].a==10)==m)//題意
{
delta+=calc();
if(exp(1.0*delta/t)<rand(0,1)) swap(q[x],q[y]);//因為是求最大值,所以前面的括號裡就沒有負號了
}
else swap(q[x],q[y]);//如果不行就交換回來
}
}
int main()
{
scanf("%d",&n);m=n;for(int i=1;i<=n;i++) scanf("%d%d",&q[i].a,&q[i].b);
if(q[n].a==10) m++,scanf("%d%d",&q[m].a,&q[m].b);
do{ simulate_anneal();} while(1.0*clock()<CLOCKS_PER_SEC*0.95);
printf("%d\n",ans);
return 0;
}
【應用】均分資料
給出 \(n\) 個數,將他們分成 \(m\) 組,求他們的最小均方差。
資料範圍
\(1 \leq m \leq n \leq 20\)。
思路
可以發現,本題直接隨機化貌似不太好做。先考慮這樣一個問題,如果當前已經將前 \(n-1\) 個數分到了 \(m\) 組裡。那麼最後一個數肯定是要放到當前數值之和最小的那組陣列當中,均方差才會最小。感性理解一下並不難,嚴格的數學證明可以請教一下N總。
那麼就又可以將原問題轉化為一個重排列的問題,和上一道題的方法就類似了。
code:
#include<ctime>
#include<cmath>
#include<cstdlib>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=110;
double ans=1e15;
int n,m,a[N],s[N];
double ave;
double calc()
{
memset(s,0,sizeof(s));
for(int i=1;i<=n;i++)
{
int k=1;
for(int j=2;j<=m;j++)
if(s[j]<s[k]) k=j;
s[k]+=a[i];
}
double res=0;
for(int i=1;i<=m;i++) res+=(1.0*s[i]-ave)*(1.0*s[i]-ave);
res=sqrt(1.0*res/m);ans=min(ans,res);
return res;
}
double rand(double l,double r){return 1.0*rand()/RAND_MAX*(r-l)+l;}
void simulate_anneal()
{
double S=1e5,T=1e-5,K=0.996;
for(double t=S;t>=T;t*=K)
{
int x=rand()%n+1,y=rand()%n+1;double delta=-calc();swap(a[x],a[y]);delta+=calc();
if(exp(-1.0*delta/t)<rand(0,1)) swap(a[x],a[y]);
}
}
int main()
{
srand(time(0));scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]),ave+=a[i];ave/=1.0*m;
do{ simulate_anneal();} while(1.0*clock()<=CLOCKS_PER_SEC*0.95);
printf("%.2lf\n",ans);
return 0;
}