『学习笔记』模拟退火

1. 简介

什么是模拟退火?(选自 OI Wiki
模拟退火是一种随机化算法。当一个问题的方案数量极大(甚至是无穷的)而且不是一个单峰函数时,我们常使用模拟退火求解。


2. 实现

模拟退火,顾名思义,是模拟「退火」的过程。当我们使用爬山算法的时候,对于非单峰函数的情形容易陷入次优解。爬山算法省略了最优解附近的非最优解从而想得到更优的答案,但是模拟退火试图以一定概率接受这个解。这个事情的实现即为「模拟退火」算法。(胡扯)
由于退火的过程有更多随机的选择因素,我们得到最优解的概率也会增加。

接受新状态概率

得到一组新解,我们有两种选择:接受或不接受。贪心告诉我们如果新状态更优就接受,否则不接受。然而模拟退火算法表示,如果新状态更优,一定接受;否则以一定概率接受。
具体来说,如果当前温度假设为 \(T\) ,新状态(由旧状态随机得到,比如交换两个数)与旧状态能量之差为 \(\Delta E \ge 0\) ,我们接受这个状态的概率为:

\[P(\Delta E)=\begin{cases}1\quad\quad\; 新状态更优\\e^{\frac{-\Delta E}{T}}\quad 新状态更劣\end{cases} \]

模拟退火过程

模拟退火中有 \(3\) 个参数:初始温度 \(T\) ,降温系数 \(d\) ,终止温度 \(T'\)

  • 一般 \(T\) 我取 1919.810 ,是一个比较大的数。
  • \(d\) 是一个接近 \(1\) 而小于 \(1\) 的数。
  • \(T'\) 是一个接近 \(0\) 的数,一般会定为 eps
    我们每次随机最优解之后,新的温度 \(T\) 赋值为旧温度 \(T\times d\)\(T\le T'\) 时模拟退火算法结束。
    模拟退火过程中我们取所有计算过的状态的最优解。

如何卡时间

C++ 里面自带一个函数 clock() ,返回的是程序运行时间(单位是微秒),除以 CLOCKS_PER_SEC 就是运行秒数。所以我们可以这样:

while (clock() / (1.0 * CLOCKS_PER_SEC) <= 0.98) solve();

3. 例题

「Luogu2210」Haywire

  • 题意:&…¥%*@&…#¥%
  • 做法:
    初始排列任意,每次挑两个排列中的奶牛交换,重新计算答案,判断是否优于当前最优解、是否转移即可。
void solve() {
    double tp = 6000;
    while (tp > eps) {
        int x = rand() % n + 1;
        int y = rand() % n + 1;
        swap(p[x], p[y]);
        int res = 0;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= 3; j++) {
                res += abs(p[i] - p[s[i][j]]);
            }
        }
        res >>= 1;
        if (res < ans) ans = res;
        else if (exp(ans - res) / tp < double(rand()) / RAND_MAX) swap(p[x], p[y]);
        tp *= d;
    }
}

「POI2008」POD-Subdivision of Kingdom

  • 题意:给出一张有 \(n\) 个点 \(m\) 条边的无向图,你需要求出一组合法的方案,使得图被划分为点数均为 \(\frac{n}{2}\) 的两个集合,且两个端点在不同集合中的边数最少。(保证 \(n\) 为偶数)
  • 做法:
    和第一题类似的交换方案
    一开始分组任意,每次退火的时候从两组里面任取一个出来交换,可以暴力计算新的答案,然后判断是否转移。
int chk(int x) { return p[x] > md; }
void solve() {
    double tp = 11451.4;
    while (tp > eps) {
        int x = rand() % md + 1;
        int y = rand() % md + md + 1;
        swap(p[t[x]], p[t[y]]);
        swap(t[x], t[y]);
        int res = 0;
        for (int i = 1; i <= m; i++) res += (chk(u[i]) ^ chk(v[i]));
        if (res < ans) {
            ans = res;
            for (int i = 1; i <= md; i++) as[i] = t[i];
        } else if (exp((ans - res) / tp) < double(rand()) / RAND_MAX) {
            swap(p[t[x]], p[t[y]]);
            swap(t[x], t[y]);
        }
        tp *= d;
    }
}

「JSOI2008」球形空间生成器

  • 题意:给定 \(n\) 维球体上 \(n+1\) 点坐标,确定这个球体的球心。
  • 做法:
    对于每个维度初始的球心坐标即对每个点坐标取平均值。然后求出每个点距离当前球心的距离,如果较远就往那个点拉,反之往反方向推。和模拟退火不同的是新的解必须接受,于是把每个维度的变化量算出来,乘以退火温度 \(T\) 加到原来每个维度的坐标上即可。
#include <bits/stdc++.h>
using namespace std;

const int maxn = 1010;
double tot, f[maxn][maxn], ans[maxn], cnt[maxn], dis[maxn];
int n;

void check() {
	tot = 0;
	for (int i = 1; i <= n + 1; i++) {
		dis[i] = cnt[i] = 0;
		for (int j = 1; j <= n; j++) dis[i] += (f[i][j] - ans[j]) * (f[i][j] - ans[j]);
		dis[i] = sqrt(dis[i]);
		tot += dis[i];
	}
	tot /= n + 1;
	for (int i = 1; i <= n + 1; i++) {
		for (int j = 1; j <= n; j++) {
			cnt[j] += (dis[i] - tot) * (f[i][j] - ans[j]) / tot;
		}
	}
}

int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n + 1; i++) {
		for (int j = 1; j <= n; j++) {
			scanf("%lf", &f[i][j]);
			ans[j] += f[i][j];
		}
	}
	for (int i = 1; i <= n; i++) ans[i] /= n + 1;
	for (double t = 1919.810; t >= 0.0001; t *= 0.99995) {
		check();
		for (int i = 1; i <= n; i++) ans[i] += cnt[i] * t;
	}
	for (int i = 1; i <= n; i++) printf("%.3f ", ans[i]);
	return 0;
}

4. 写在后面

模拟退火真™是个玄学东西。
有兴趣的可以做做 这个题单
祝你在考场上写的退火都拿 \(\color{lightgreen}{\mathtt{100pts}}\)!(

posted @ 2022-01-09 14:42  Ender_32k  阅读(581)  评论(2编辑  收藏  举报