模拟退火学习笔记
【前言】
好文章先放这:OI wiki - 模拟退火。
模拟退火是一个著名的 玄学 算法,理论上来说 只要欧气爆棚就 能解决所有最优化问题。
【主要思想】
【随机数】
一开始就是玄学
模拟退火本质上依赖的是随机化,所以随机数的生成是必须的。
-
srand(time(0))
,就是个随机数种子,放在主函数开头就行了。 -
DB Rand1(){return (DB) rand() / RAND_MAX;}
,用于生成一个 \(< 1\) 的随机小数, -
int Rand2(int x){return rand() % x + 1;}
,用于生成一个在 \([1,x]\) 范围内的数。
【模拟退火】
首先得有一个初始温度 \(t\),控制的是模拟退火次数,理论上来说越多越好,但你肯定不能超时。
然后每次退火,令 \(t\times {\rm down}\),其中 \(\rm down\) 是一个常数,通常取 \([0.990\sim 0.999]\),目的是模拟慢慢退火的过程。
然后在每次退火的过程中,随机地扰动,得到一个新状态。
值得一提的是,随机的扰动的增量范围通常与 \(t\) 成正比,也就是说温度越低,扰动幅度越小。
然后用一句话概括:如果新状态的解更优则修改答案,否则以一定概率接受新状态。
至于概率是什么,当然也是随机了。
根据 玄学 科学计算,代码一般长这样:if(exp(-delta) / t < Rand1()) ...
,其中 ...
表示撤销改动。
【可能的优化】
- 退火结束后再跑个 \(1000\) 次左右,有更大的概率得到答案。
- 为了 保证正确率 并且 不超时,可以选择卡时:
while ((double)clock()/CLOCKS_PER_SEC < 0.80)
。 - 调参很关键,一般可以根据大样例手动二分。
然后说一点个人习惯:
- \(t\) 取 \(10^5\) 左右。
- \(\rm eps\) 取 \(1^{-10}\) 左右。
- \(\rm down\) 取 \(0.997\) 左右。
【适用情况】
- 你实在想不到正解。
- 是最优化问题,而且数据范围通常比较小(便于计算代价)。
题目看上去就很玄学。
【代码实现】
例题:平衡点 / 吊打XXX。
求一个坐标 \((x,y)\) 使 \(\sum_{i=1}^n dis((x_i,y_i),(x,y))\times w_i\) 最小。
看上去就很模拟退火,直接上代码吧。
#include<cstdio>
#include<algorithm>
#include<cstdlib>
#include<ctime>
#include<cmath>
using namespace std;
const int N = 1010;
int n, x[N], y[N], w[N];
double ansx, ansy, dis;
int read(){
int x=0,f=1;char c=getchar();
while(c<'0' || c>'9') f=(c=='-')?-1:1,c=getchar();
while(c>='0' && c<='9') x=x*10+c-48,c=getchar();
return x*f;
}
double Rand(){return (double) rand() / RAND_MAX;}
double calc(double xx, double yy){
double res = 0.0;
for(int i = 1; i <= n; i ++){
double dx = xx - x[i], dy = yy - y[i];
res += sqrt(dx * dx + dy * dy) * w[i];
}
if(res < dis) dis = res, ansx = xx, ansy = yy;
return res;
}
void work(){
double t = 100000;
double nowx = ansx, nowy = ansy;
while(t > 0.001){
double nxtx = nowx + t * (Rand() * 2 - 1);
double nxty = nowy + t * (Rand() * 2 - 1);
double delta = calc(nxtx, nxty) - calc(nowx, nowy);
if(exp(-delta / t) > Rand()) nowx = nxtx, nowy = nxty;
t *= 0.997;
}
for(int i = 1; i <= 1000; i ++){
double nxtx = ansx + t * (Rand() * 2 - 1);
double nxty = ansy + t * (Rand() * 2 - 1);
calc(nxtx, nxty);
}
}
int main(){
srand(time(0));
n = read();
for(int i = 1; i <= n; i ++){
x[i] = read(), y[i] = read(), w[i] = read();
ansx += x[i], ansy += y[i];
}
ansx /= n, ansy /= n, dis = calc(ansx, ansy);
work();
printf("%.3lf %.3lf\n", ansx, ansy);
return 0;
}
由于算法本身的不稳定性,或许你要交很多遍才能过。
【简单例题】
求 \(n\) 个数的排列顺序,使得有关联的数之间的距离差距和最小,一个数与三个数有关系,关系是相互的。
每次随机交换两个数的位置即可。
#include<cstdio>
#include<algorithm>
#include<cstdlib>
#include<ctime>
#include<cmath>
using namespace std;
typedef double DB;
const DB down = 0.997;
int n, ans, pos[15], a[15][5];
int read(){
int x=0,f=1;char c=getchar();
while(c<'0' || c>'9') f=(c=='-')?-1:1,c=getchar();
while(c>='0' && c<='9') x=x*10+c-48,c=getchar();
return x*f;
}
DB Rand1(){return (DB) rand() / RAND_MAX;}
int Rand2(int x){return rand() % x + 1;}
int calc(){
int rec = 0;
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= 3; j ++)
rec += abs(pos[i] - pos[a[i][j]]);
rec >>= 1;
ans = min(ans, rec);
return rec;
}
void work(){
DB t = 100000, eps = 1e-16;
int now = ans;
while(t > eps){
int x = Rand2(n), y = Rand2(n);
swap(pos[x], pos[y]);
int nxt = calc();
int delta = nxt - now;
if(delta < 0) now = nxt;
else if(exp(-delta) / t < Rand1()) swap(pos[x], pos[y]);
t *= down;
}
for(int i = 1; i <= 1000; i ++){
int x = Rand2(n);
int y = Rand2(n);
swap(pos[x], pos[y]);
calc();
}
}
int main(){
srand(time(0));
n = read();
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= 3; j ++) a[i][j] = read();
for(int i = 1; i <= n; i ++)
pos[i] = i;
ans = calc();
work();
printf("%d\n", ans);
return 0;
}
还有些简单习题:
完结撒花。