学习笔记:模拟退火

模拟退火大概就是一个随机化求最优解的问题。

考虑一个较连续的多峰的函数,用模拟退火可以较大几率找到极值,具体过程(这里假设找的是最小值,最大值反着弄一下就行了):

  1. 初始一个温度 / 步长 (T),随机一个点

  2. 在步长范围内随机选一个新点,记能量变化量 (ΔE) 为新点值 (-) 旧点值。

    • (ΔE < 0) 就跳到新点(更优)
    • 否则一定概率跳到新点(!)

    这个概率一般取 (e^{-frac{ΔE}{T}}),即判断 (e^{-frac{ΔE}{T}} > rand(0, 1)) 作为跳不跳的条件,(rand(0, 1)) 表示 (0 sim 1) 的随机数。

    (考虑 (< 0) 里面这个东西 (>1),如果 (>0) 那么它越靠近 (0) 概率越高。

  3. 缩小步长 (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
", 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
", sqrt(ans));
	return 0;
}
原文地址:https://www.cnblogs.com/dmoransky/p/14274471.html