模拟退火大概就是一个随机化求最优解的问题。
考虑一个较连续的多峰的函数,用模拟退火可以较大几率找到极值,具体过程(这里假设找的是最小值,最大值反着弄一下就行了):
-
初始一个温度 / 步长 \(T\),随机一个点
-
在步长范围内随机选一个新点,记能量变化量 \(ΔE\) 为新点值 \(-\) 旧点值。
- 若 \(ΔE < 0\) 就跳到新点(更优)
- 否则一定概率跳到新点(!)
这个概率一般取 \(e^{-\frac{ΔE}{T}}\),即判断 \(e^{-\frac{ΔE}{T}} > rand(0, 1)\) 作为跳不跳的条件,\(rand(0, 1)\) 表示 \(0 \sim 1\) 的随机数。
(考虑 \(< 0\) 里面这个东西 \(>1\),如果 \(>0\) 那么它越靠近 \(0\) 概率越高。
-
缩小步长 \(T\) (一般乘一个 \(0.99\) 之类的东西),重复步骤 \(2\) 直到步长足够小(在输出精度误差范围内)。
正确性十分玄学,不过在重复多次的情况下错误率极低(具体可以通过峰值,循环,幂次的形式自己算一算)
例题
POJ2420:二维自变量的连续函数,板子。
#include <iostream>
#include <cstdio>
#include <ctime>
#include <cmath>
#include <cstdlib>
using namespace std;
const int N = 105;
const double eps = 1e-5;
double inline rand(int l, int r) {
return (double)rand() / RAND_MAX * (r - l) + l;
}
int n, X[N], Y[N];
double ans = 1e18, s;
double inline get(double x, double y) {
double res = 0;
for (int i = 1; i <= n; i++) res += sqrt((x - X[i]) * (x - X[i]) + (y - Y[i]) * (y - Y[i]));
ans = min(ans, res);
return res;
}
void inline simulateAnneal() {
double x = rand(0, 10000), y = rand(0, 10000), v = get(x, y);
for (double t = 10000; t > eps; t *= 0.99) {
double nx = rand(x - t, x + t), ny = rand(y - t, y + t), nv = get(nx, ny);
if (exp(-(nv - v) / t) > rand(0, 1)) x = nx, y = ny, v = nv;
}
}
int main() {
s = clock();
srand(time(0));
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d%d", X + i, Y + i);
while (((double)clock() - s) / CLOCKS_PER_SEC < 0.8) simulateAnneal();
printf("%.0f\n", ans);
}
AHOI2014 保龄球:交换两个位置差距不会太大较为连续,板子。
#include <iostream>
#include <cstdio>
#include <ctime>
#include <cstdlib>
#include <cmath>
using namespace std;
const int N = 55;
int n, X[N], Y[N], m, p[N];
int ans = 0, sum;
int inline get() {
int res = sum;
for (int i = 1; i < m; i++)
if (X[p[i]] == 10) res += X[p[i + 1]] + Y[p[i + 1]];
else if (X[p[i]] + Y[p[i]] == 10) res += X[p[i + 1]];
ans = max(ans, res);
return res;
}
double inline rd() {
return (double)rand() / RAND_MAX;
}
void inline simulateAnneal() {
int v = get();
for (double t = 1e4; t > 1e-4; t *= 0.99) {
int a = rand() % m + 1, b = rand() % m + 1;
swap(p[a], p[b]);
if (n + (X[p[n]] == 10) == m) {
int nv = get();
if (exp(-((double)v - nv) / t) <= rd()) swap(p[a], p[b]);
else v = nv;
} else swap(p[a], p[b]);
}
}
int main() {
srand(time(0)); double s = clock();
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d%d", X + i, Y + i);
if (X[n] == 10) scanf("%d%d", X + n + 1, Y + n + 1);
m = n + (X[n] == 10);
for (int i = 1; i <= m; i++) sum += X[i] + Y[i], p[i] = i;
while ((clock() - s) / CLOCKS_PER_SEC < 0.8) simulateAnneal();
printf("%d", ans);
return 0;
}
HAOI2006 均分数据:考虑一种贪心方式,每次按照一定顺序,每个数放最小和里,考虑肯定有顺序能构成最优解,而且这题数据水的一批,因此乱搞一下就过了。。
#include <iostream>
#include <cstdio>
#include <ctime>
#include <cstdlib>
#include <cmath>
using namespace std;
const int N = 21;
int n, m, a[N], b[N], p[N];
double ans = 9e18;
int inline get() {
for (int i = 1; i <= m; i++) b[i] = 0;
for (int i = 1; i <= n; i++) {
int t = 1;
for (int j = 2; j <= m; j++) if (b[j] < b[t]) t = j;
b[t] += a[p[i]];
}
double s = 0;
for (int i = 1; i <= m; i++) s += b[i];
s /= m;
double res = 0;
for (int i = 1; i <= m; i++) res += (b[i] - s) * (b[i] - s);
res /= m;
ans = min(ans, res);
return res;
}
double inline rd() {
return (double)rand() / RAND_MAX;
}
void inline simulateAnneal() {
int v = get();
for (double t = 1e3; t > 1e-2; t *= 0.9) {
int a = rand() % n + 1, b = rand() % n + 1;
swap(p[a], p[b]);
int nv = get();
if (exp(-((double)nv - v) / t) <= rd()) swap(p[a], p[b]);
else v = nv;
}
}
int main() {
srand(time(0)); double s = clock();
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) scanf("%d", a + i), p[i] = i;
simulateAnneal();
printf("%.2f\n", sqrt(ans));
return 0;
}