模拟退火学习笔记
爬山算法在单峰函数中比较优秀,但是当函数不是单峰时很容易陷入局部最优解,此时需要用模拟退火(Simulate Anneal)。
一般来讲,当问题规模非常巨大常规算法难以解决且求的是最优解问题,或者是你根本不会正解想要骗分时,模拟退火非常好用。
模拟退火关键在于几个参数:初始温度 $t_0$,终止温度 $t_k$,降温系数 $t_d$,停止反复退火时限 $MAXTIME$,其中 $t_0$ 是一个比较大的数,$t_k$ 是一个接近于 $0$ 但大于 $0$ 的数,$t_d$ 是一个接近于 $1$ 但小于 $1$ 的数,$\log_{t_d} \frac{t_k}{t_0}$ 是退火时温度变化的次数,与单次退火的时间复杂度有关,$MAXTIME$ 是一个接近于题目要求的时限的数,使执行退火的次数尽可能多。
模拟退火的流程为:首先让温度 $t=t_0$,再根据当前状态在其附近随机一个新状态,如果新状态更优则修改答案,否则以一定概率选择是否跳到新状态(一般取概率为 $e^{-\frac{\Delta}{t}}$,其中 $\Delta$ 为新状态与之前状态的答案之差),然后将温度 $t\leftarrow t\times t_d$ 来降温,不断反复这个过程直到 $t<t_k$,最后在当前找到的最优解附近多次随机新状态,尝试得到更优的解。
不难发现温度越高新状态越容易远离当前局部最优解,降温的过程就是将搜寻最优解的范围逐渐缩小并逐渐锁定最优解的过程。
我们维护的答案是整个退火过程中的最优解,我们需要在不超时的前提下调整 $MAXTIME$ 来尽可能多地退火使解尽量精确。
模拟退火是一个非常玄学的随机算法,调整参数对其找最优解的准确度影响非常大,尤其是 $t_d$,它越大退火越慢,单次找最优解的准确度越大但执行退火的次数也越少,因此经过反复调参后的模拟退火往往能够骗到高分但是面对比较强的数据很难全部 AC。
例题:
1 const int N = 1e3 + 10; 2 const double MAXVAL = 1ull << 32, MAXTIME = 0.977; 3 const double t0 = 1e3, tk = 1e-3, td = 0.997; 4 int n; 5 double ansx, ansy, ansr, x[N], y[N], w[N]; 6 mt19937 get_rand(time(0)); 7 8 inline double Rand() { return 1.0 * get_rand() / MAXVAL; } 9 double calc(double xx, double yy) 10 { 11 double rr = 0.0; 12 for (int i = 1; i <= n; ++i) 13 rr += sqrt((x[i] - xx) * (x[i] - xx) + (y[i] - yy) * (y[i] - yy)) * w[i]; 14 if (rr < ansr) ansx = xx, ansy = yy, ansr = rr; 15 return rr; 16 } 17 void Simulate_Anneal() 18 { 19 double t = t0, ux = ansx, uy = ansy, ur = ansr; 20 while (t > tk) 21 { 22 double vx = ux + t * (2.0 * Rand() - 1.0); 23 double vy = uy + t * (2.0 * Rand() - 1.0); 24 double vr = calc(vx, vy), delta = vr - ur; 25 if (exp(-delta / t) > Rand()) ux = vx, uy = vy, ur = vr; 26 t *= td; 27 } 28 for (int i = 1; i <= N; ++i) 29 calc(ansx + t * (2.0 * Rand() - 1.0), ansy + t * (2.0 * Rand() - 1.0)); 30 } 31 void solve() 32 { 33 while (1.0 * clock() / CLOCKS_PER_SEC < MAXTIME) Simulate_Anneal(); 34 } 35 36 int main() 37 { 38 read(n); 39 for (int i = 1; i <= n; ++i) 40 read(x[i], y[i], w[i]), ansx += x[i], ansy += y[i]; 41 ansx /= n, ansy /= n, ansr = calc(ansx, ansy); 42 solve(); printf("%.3lf %.3lf", ansx, ansy); 43 flush_pbuf(); return 0; 44 }
To be continued...