模拟退火
概述
简单说,模拟退火是一种随机化算法,用于求函数的极值。当一个问题的方案数量极大(甚至是无穷的)而且不是一个单峰函数时,我们常使用模拟退火求解。
它与爬山算法最大的不同是,在寻找到一个局部最优解时,赋予了它一个跳出去的概率,也就有更大的机会能找到全局最优解。
在 OI 领域,对应的,每次随机出一个新解,如果这个解更优,则接受它,否则以一个与温度和与最优解的差相关的概率接受它。
相关参数
初始温度:(T_0)
结束温度:(T_s)
降温系数:(Delta t)
这样每次温度就是上次的温度乘上(Delta t)
能量差:(Delta E=f(t_{new})-f(t_{now})),即新点的能量减去当前的能量(能量也就是函数值)
接受概率:(P(Delta E)=e^{frac{-Delta E}{t}}),t为当前温度,这样保证当能量差小于0时,概率P是大于1的,也就是必然接受,当能量差大于0时,能量差越大越不容易接受,t越大越容易接受(这里能量差需要具体问题具体分析,因为是退火,所以能量是越小越好)
模板
AcWing 3167. 星星还是树
二维平面有若干点,寻找一个点到所有点的距离之和最短,该点可以选择在平面中的任意位置,甚至与这 n 个点的位置重合。
#include <bits/stdc++.h>
using namespace std;
#define pdd pair<double, double>
int const N = 100 + 5;
vector<pdd> a;
const double inf = 2e6;
double ans = inf;
double getd(pdd x, pdd y) {
double dx = x.first - y.first;
double dy = x.second - y.second;
return sqrt(dx * dx + dy * dy);
}
double getsum(pdd x) { //f函数
double re = 0;
for (auto& p : a) {
re += getd(x, p);
}
ans = min(ans, re);
return re;
}
double rand(double l, double r) { //计算一个l到r的随机值
return (double)rand() / RAND_MAX * (r - l) + l;
}
void sa() {
pdd p(rand(0, 1e4), rand(0, 1e4));
for (double t = 1e4; t > 1e-4; t *= 0.99) {
pdd np(rand(p.first - t, p.first + t),
rand(p.second - t, p.second + t));//随机一个新的点
double dt = getsum(np) - getsum(p); //计算能量差
if (exp(-dt / t) > rand(0, 1)) {
p = np;
}
}
}
int main() {
int n;
cin >> n;
srand((unsigned)time(NULL));
for (int i = 0; i < n; ++i) {
double x, y;
cin >> x >> y;
a.push_back({x, y});
}
for (int i = 0; i < 100; ++i) sa();
cout << round(ans) << "
";
}
技巧
1.由于较为玄学,所以需要多跑几次模拟退火
2.更改随机种子
3.卡时间,例如小于0.8秒就一直跑模拟退火,充分利用测评时间
4.超时说明降温系数太大了,降温过程太慢,方法:可以把0.999改为0.99