模擬退火大概就是一個隨機化求最優解的問題。
考慮一個較連續的多峰的函數,用模擬退火可以較大幾率找到極值,具體過程(這里假設找的是最小值,最大值反着弄一下就行了):
-
初始一個溫度 / 步長 \(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;
}