Loading

抽象的模拟退火算法(Simulated Annealing,SA)学习笔记

介绍

首先希望大家点赞评论,谢谢!

模拟退火是一种随机算法,由为了使模拟退火能被使用,必须满足答案只与顺序有关。

这是一种随机性算法,适用于普通爆搜用不了的情况。其抽象在要不断的取随机数,所以这种算法最核心的地方在于乱取随机数,直到达到最优解。

听上去很抽象是吗?但是它的随机也是有规律的随机,我们将不断的重复退火的过程,单次退火后随机转移的概率会越来越低。这就是 SA 从不稳定到稳定的过程

算法过程

我们要有个初始温度 \(T\),热力学满分的童鞋都知道,\(T\) 越高的时候,粒子也就越不稳定。

OI-WIKI 上的描述稍显专业,个人觉得本文所述更易理解一点。大家也可以参考

以排列最优问题为例,SA 的单次迭代,会随机选择两个初始排列的数,交换它们。调用 get 函数做计算:有两种情况,在较之前已有情况更优时,直接修改答案,并且保留已经交换的数。如果不优,那么以一定的概率保持之前修改了的状态,这个概率是多少呢?我们设 \(\rm tmp\) 为此次的答案,\(\rm ans\) 为目前最优的答案。那么这里 \(\Delta E=\rm ans- tmp\),即能量差,则有:\(\mathrm{e}^{-\frac{\Delta E}{T}}\) 的转移概率。

单词迭代完成后,\(T\) 要乘以一个小于 \(1\) 的固定系数,即“退火”,等待下一次迭代。

迭代到什么时候结束呢?迭代当 \(T\) 达到一个十分接近 \(0\) 的值时结束。

模拟退火模板

单说可能会有点抽象,一下是模板代码:

double Rand() {
  // 可以获得小于等于 1 的随机数
  return (double)rand() / RAND_MAX;
}

void sa() {
  double t = 1000; // 可以根据需要修改
  while (t > 1e-12) { // 可以根据需要修改
    // 随机转移
    double tmp = get(); // 自设的计算函数
    if (tmp < ans) {
      ans = tmp; // 默认转移,并设 ans 为目前最优解
    } else if (exp((ans - tmp) / t) > Rand()){
      // 随机下来无法转移,撤销转移
    }
    t *= 0.996; // 可以根据需要修改
  }
}

卡时技巧

为了保证答案准确性,一般要多做很多次 SA,以下代码可以保证不超时的情况下达到正确率最大化:

while((double)clock() / CLOCKS_PER_SEC <= time_limit) sa();
// time_limit 为题目限时,建议预留 100ms - 200ms,防止出现超时

习题

最近在做油滴扩展,本是一道可以暴力水过的题,但还是给大家看一下代码,Luogu P1378:

#include <bits/stdc++.h>
#define rty printf("Yes\n");
#define RTY printf("YES\n");
#define rtn printf("No\n");
#define RTN printf("NO\n");
#define rep(v,b,e) for(int v=b;v<=e;v++)
#define repq(v,b,e) for(int v=b;v<e;v++)
#define rrep(v,e,b) for(int v=b;v>=e;v--)
#define rrepq(v,e,b) for(int v=b;v>e;v--)
#define stg string
#define vct vector
using namespace std;

typedef long long ll;
typedef unsigned long long ull;

void solve() {
	
}

const double pi = 3.141592653589793;
const double e = 2.718281;

int n, zx, yx, sy, xy;
int objx[10], objy[10];
double radius[10];
double ans = 0;

double dis(double x1, double y1, double x2, double y2) {
  return sqrt((x1 - x2)*(x1 - x2) + (y1 - y2)*(y1 - y2));
}

double Rand() {
  return (double)rand() / RAND_MAX;
}

double get() {
  double sum = 0;
  rep(i, 1, n) {
    radius[i] = 
      min(
        {
          abs(yx - objx[i]),
          abs(zx - objx[i]),
          abs(sy - objy[i]),
          abs(xy - objy[i])
        }
      );
    repq(j, 1, i) {
      double di = dis(objx[i], objy[i], objx[j], objy[j]);
      if (di >= radius[j]) { 
        radius[i] = min(radius[i], di - radius[j]);
      } else radius[i] = 0;
    }
    sum += radius[i] * radius[i] * pi;
  }
  return abs(yx - zx) * abs(sy - xy) - sum; 
}

void sa() {
  double t = 1000;
  while (t > 1e-12) {
    int a = rand() % n + 1;
    int b = rand() % n + 1;
    swap(objx[a], objx[b]); swap(objy[a], objy[b]);
    double tmp = get();
    if (tmp < ans) {
      ans = tmp;
    } else if (pow(e, (ans - tmp) / t) < Rand()){
      swap(objx[a], objx[b]); swap(objy[a], objy[b]);
    }
    t *= 0.996;
  }
}

main() {
//	int t; cin >> t; while (t--) solve();
  srand(time(NULL));
  cin >> n; 
  cin >> zx >> sy >> yx >> xy;
  if (yx > zx) swap(yx,zx);
  if (sy > xy) swap(sy, xy);
  rep(i, 1, n) cin >> objx[i] >> objy[i];
  ans = get();
  while((double)clock() / CLOCKS_PER_SEC <= 0.9) sa();
  printf("%.0lf\n", ans);
	return 0;
}

还有一些习题,详见 OI-WIKI。

最后

感谢大家的阅读,希望能点赞评论支持一下,谢谢!

posted @ 2024-08-09 12:07  CoutingStars  阅读(16)  评论(0编辑  收藏  举报