玄学乱搞算法——模拟退火,SA
前言
在此之前只对模拟退火的大名有所耳闻,但并未在我的认知上激起太大的风浪,直到……
在外培的一场模拟赛上,队内大佬 yyc 在丝毫没有思路的情况下用 SA 骗了 70pts,赛后使得给我们上课的清华姚班老师惊掉下巴。
至此,在感叹 SA 的神力的同时,它也进入了我的学习计划中。
介绍
退火原本是一个物理概念,是指一种热处理工艺,将固体材料加热到一定温度后,让其缓慢冷却的过程。
模拟退火算法(Simulate Anneal,SA)是一种通用概率演算法,用来在一个大的搜寻空间内找寻命题的最优解。模拟退火是由 S.Kirkpatrick,C.D.Gelatt 和 M.P.Vecchi 在1983年所发明的。V.Černý 在 1985 年也独立发明此演算法。模拟退火算法是解决 TSP 问题的有效方法之一。——百度百科
模拟退火是一种随机化算法。当一个问题的方案数量极大(甚至是无穷的)而且不是一个单峰函数时,我们常使用模拟退火求解。
简单来讲,模拟退火是一种启发式搜索,它通过模拟物理降温的过程,先随机取一个起始点,然后随机地搜索其他点,若更优就跳过去,否则以一定的概率跳过去。由于模拟退火的过程中有概率接受较劣的解,所以它有一定概率找到全局最优解而不是受困于某一局部最优解。
算法过程
首先设定几个参数:初温
在算法开始时钦定温度
具体来讲,在找最优解时的准则:如果新状态的解更优则修改答案,否则以一定概率接受新状态。
我们定义当前温度为
注意为了使得解更为精确,我们通常不直接取当前解作为答案,而是在退火过程中维护遇到的所有解的最优值。
注意:模拟退火只能用来求解具有一定连续性的函数的最优解,这个连续性可以具体看题目中一个微小的策略改变是否导致了函数值的剧变,若导致了,则没多少连续性,否则可以认为有一定连续性。
模拟退火的过程如图所示:
void SA() {
double T = 1e3;
while(T > 1e-7) {
//do something...
ll now = calc(); //求当前的函数值
ll dt = now - ans; //ans 是全局最优解
if(dt < 0) ans = now; //更优则直接接受
else if(exp(-(double)dt / T) * RAND_MAX < (double)rand())
//不是更优则以一定概率接受
T *= k;
}
}
一些例题
P1337 [JSOI2004] 平衡点 / 吊打XXX
题目大意
给定
思路
这道题正解应该是三分套三分,但是不会,考虑乱搞。
根据基本的物理知识,平衡点会偏向重物所在的地方,事实上,假如钦定一个点
同时在点
这里我们顺便用当前温度代表当前的步长(随机的范围),便能轻松写出如下代码:
#include <cmath>
#include <ctime>
#include <iostream>
#define x first
#define y second
using namespace std;
const int N = 1010;
const double eps = 1e-9;
typedef pair<double, double> PDD;
int n;
struct node{
int xx, yy, w;
}q[N];
double ans = 1e18;
int sumx, sumy;
PDD anss;
inline double rand(double l, double r) {
return (double)rand() / RAND_MAX * (r - l) + l;
}
inline double get_dist(PDD a, PDD b) {
return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}
inline double calc(PDD now) {
double res = 0;
for(int i = 1; i <= n; i++)
res += get_dist(now, {q[i].xx, q[i].yy}) * q[i].w;
return res;
}
void SA() {
PDD now(rand(-10000.0, 10000.0), rand(-10000.0, 10000.0));
double T = 1e4;
while(T > 1e-5) {
PDD newp(now.x + (double)((rand() << 1) - RAND_MAX) * T, now.y + (double)((rand() << 1) - RAND_MAX) * T);
double fut = calc(newp);
double dt = fut - ans;
if(dt < 0) ans = fut, anss = newp, now = newp;
else if(exp(-dt / T) * RAND_MAX > (double)rand()) now = newp;
T *= 0.95;
}
}
int main() {
srand((unsigned)time(0));
scanf("%d", &n);
for(int i = 1; i <= n; i++) {
scanf("%d%d%d", &q[i].xx, &q[i].yy, &q[i].w);
}
SA();
printf("%.3lf %.3lf", anss.x, anss.y);
return 0;
}
虽然函数都写对了,但是这样连样例都过不了,怎么办?
其实有模拟退火的三大调整策略:
-
改进随机数的生成或是用贪心等策略优化找最优解的过程;
-
在时间允许的条件下,可以适当调高降温系数
,调整初温或末温,还可以多跑几遍,甚至卡时;(调参是模拟退火最好玩的一环,不爽不要玩!) -
采用精度更高的数据类型,比如 long double。(其实这条没什么用,主要看前两条)
由于温度和步长有关,所以这道题可以将末温调低一点,同时时间很充裕,所以可以调大降温系数和多跑几遍。
这样我们就 AC 了这道题。
其实还可以将起点设为固定点中的平均值,这样更容易跑出最优解。
P2503 [HAOI2006] 均分数据
题目大意
给定一个长为
思路
因为数据范围比较小,所以其实各种乱搞都能过(
考虑模拟退火。
比较朴素的一种想法是,每次找最优解时就随机将这
但是这样不容易搜到最优解,因为可能将许多大数丢到同一个集合去,所以很多情况都没有用。
综上,我们可以加一个贪心优化,具体地,先随机打乱原序列
#include <cmath>
#include <ctime>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 25, M = 10;
int n, m;
int a[N], s[M];
double ans = 1e9;
double calc() {
memset(s, 0, sizeof s);
for(int i = 1; i <= n; i++) {
int pos = 1;
for(int j = 2; j <= m; j++)
if(s[j] < s[pos]) pos = j;
s[pos] += a[i];
}
double avg = 0;
for(int i = 1; i <= m; i++) avg += 1.0 * s[i] / m;
double res = 0;
for(int i = 1; i <= m; i++) res += (s[i] - avg) * (s[i] - avg);
res = sqrt(res / m);
ans = min(ans, res);
return res;
}
void SA() {
random_shuffle(a + 1, a + n + 1);
double T = 1e4;
while(T > 1e-6) {
int x = rand() % n + 1, y = rand() % n + 1;
if(x == y) continue;
// printf("%d %d\n", x, y);
double now = calc();
swap(a[x], a[y]);
double fut = calc();
double dt = fut - now;
if(exp(-dt / T) * RAND_MAX < (double)rand())
swap(a[x], a[y]);
T *= 0.99;
}
}
int main() {
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
for(int i = 0; i < 10; i++) SA();
printf("%.2lf", ans);
return 0;
}
P4044 [AHOI2014/JSOI2014] 保龄球
题目大意
一场保龄球比赛共有
每一轮中,选手都有两次投球的机会来尝试击倒这
对于每一次投球机会,选手投球的得分等于这一次投球所击倒的木瓶数量。
选手每一轮的得分是他两次机会击倒全部木瓶的数量。
对于每一个轮次,有如下三种情况:
“全中”:若选手第一次尝试就击倒了全部
“补中”:若选手两次尝试击倒了
“失误”:若选手未能通过两次尝试击倒全部的木瓶,则“失误”。失误没有奖励。
此外,如果第
附加轮的规则只执行一次。
最后,选手的总得分就是附加轮规则执行过,并且分数按上述规则加倍后的每一轮分数之和。
JYY 刚刚进行了一场
JYY 希望通过更改每一轮成绩的顺序来最大化他的得分。
当然了,JYY 不希望做的太假,他希望保证重新排列之后,所需要进行的轮数和重排前所进行的轮数是一致的:
比如如果重排前 JYY 在第
求 JYY 可以得到的最高的分数。
(题目太长了,已经尽力简化了)
思路
这道题的数据范围同样较小,所以可以乱搞(
考虑模拟退火。
先看一下有没有
#include <cmath>
#include <ctime>
#include <iostream>
#define x first
#define y second
using namespace std;
const int N = 55;
typedef pair<int, int> PII;
int n, nn;
PII q[N];
int ans;
inline void swap(PII &x, PII &y) {
PII t = y;
y = x;
x = t;
}
inline int calc() {
int res = 0;
for(int i = 1; i <= nn; i++) {
res += q[i].x + q[i].y;
if(i <= n) {
if(q[i].x == 10) res += q[i + 1].x + q[i + 1].y;
else if(q[i].x + q[i].y == 10) res += q[i + 1].x;
}
}
ans = max(ans, res);
return res;
}
void SA() {
double T = 1e6;
while(T > 1e-6) {
int x = rand() % nn + 1, y = rand() % nn + 1;
int now = calc();
swap(q[x], q[y]);
if(n + (q[n].x == 10) == nn) {
int fut = calc();
int dt = fut - now;
if(exp(dt / T) < (double)rand() / RAND_MAX)
swap(q[x], q[y]);
}
else swap(q[x], q[y]);
T *= 0.996;
}
}
int main() {
srand((unsigned)(time(0)));
srand((unsigned)rand() * rand()); //玄学种子
scanf("%d", &n);
for(int i = 1; i <= n; i++) scanf("%d%d", &q[i].x, &q[i].y);
if(q[n].x == 10) nn = n + 1, scanf("%d%d", &q[nn].x, &q[nn].y);
else nn = n;
for(int i = 1; i < 3; i++) SA();
printf("%d\n", ans);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效