模拟退火算法 & BZOJ3680
0x00 一本正经的胡说八道
模拟退火算法是个很好玩儿的算法,而且打起来也很方便,没有学习这个算法之前,我对于搜索的理解大概有以下几种:BFS & DFS
分别以栈、队列形式模拟的搜索方式,几乎是所有搜索方式的框架所在,但是一旦搜索扩展状态增多,几乎鲜有可以使用纯暴力搜索实现的情况(DFS在完全扩展状态下是指数级别的复杂度,BFS遇到大的状态爆队列是妥妥的),一般来讲都要通过剪枝来进行优化。爬山算法
取得局部最优解的方法,每次搜索时都选择扩展状态中最优的那一个,当发现一个状态比其扩展状态都优的时候就停止搜索,并输出该状态。由于ACM中的问题一般都不会像这种搜索方法一样目光短浅,所以可使用的场所一般很少。记忆化搜索
原理和动态规划一样,要领在于归纳出状态并保存,并重复利用已经保存的状态,对于没有历史记录的状态分解并继续搜索(其实动态规划就是从最基础的状态向后推,记忆化搜索就是从最后的状态往前递归)。对于整理出动态规划的状态转移方程很有用,所以使用途径也非常广。IDA* & A*
区别在于是否用迭代加深实现,都是对于子状态进行权重判断,并优先选择权重最高的子状态进行搜索。(对于八数码,还原魔方,K短路这种问题就是神技)。
这些算法都是不盲目的搜索,巧妙的搜索,智慧的搜索,对于目标有着明确的规划。
但在旅行商问题面前,统统20块,统统20块,统统20块。有一个推销员,要到n个城市推销商品,他要找出一个包含所有n个城市的具有最短路程的环路。
懵逼了吧,启发函数怎么写,局部最优在哪里,状态怎么保存??
这时候就来感受一发
(模拟退火算法)的伟大吧。
随机这种东西,貌似在ACM里好像就是用于骗分……在我们机房里,曾经拥有用
AC题目的传奇级人物Ran神,但其实
有更多实际的用处(用
代替
防止快排退化,用
构建Treap树保持二叉搜索树的平衡等等),还有就是模拟退火算法,也算是随机化算法中一个极好的例子吧。对不加约束的随机有了收敛,就像不加约束的野马有了缰绳 —— 沃·兹吉硕德
0x01 算法思想
在讲模拟退火算法的时候,先将一个小明爬山的故事,这是这篇文章:大白话解析模拟退火算法给我的极大启发,之前总是被热力学的一堆名词搞得一脸懵逼。
有一天小明同学去爬山
小明左看右看,看到右边有一个山峰,小明很开心,这就是我的目标!
小明爬啊爬,爬到了山顶A。
小明再次左看右看,虽然右边有个更高的山头B,但是小明已经认为自己爬到了最高的地方,小明很开心,小明回家了。
-------------
分割线-------------
第二天小明喝醉了酒,继续来爬这座山,由于小明头脑不清醒,他开始随机的乱走,有可能在去山顶的路上。也有可能在下山的过程中。
一开始小明醉的找不着北,步伐简直像是随机乱跳,好、但后来酒渐渐消去,小明最终意识到了山顶的存在,到达了山顶。
喝醉的小明和普通的小明其实就是爬山算法和模拟退火算法的区别,爬山算法不允许出现出现解比原来不优的情况,而模拟退火算法允许在一定的概率内出现比当前状态不优的状态,也正是这种随机,使得搜索从一种坐井观天的状态中跳出,找到真正是全局最优的解。(难道喝醉更好吗23333)
用
表示对于当前状态的估价,
表示对下一个状态的估价。当
时,这自然是最好的,这种转移是允许的。
当
,但是有一个奇异的状态产生时,这种转移也会允许(但是产生这种奇异状态的概率会越来越小,毕竟小明越来越清醒了)。
而这个奇异状态的出现,来自于热力学的公式
,也就是说温度
(一开始这个温度为充分大)越高,出现温差值
为降温的概率就越大,反之就越小,而这个温差来自于
和
两个状态之间的差值,由于这个差值小于0,温度
大于0,所以
。
温度
一直一步步降低,结果也一步步趋于稳定,当
时,结束搜索,输出结果。
0x03 伪代码
while(T > T_MIN) {// 温度还可以降温dE = S(NEXT) - S(NOW);
if(dE >= 0 || exp(dE / T) > random(0, 1)) // state 1 or 2S(NOW) = S(NEXT);
T *= c; // 降温的过程}
框架很简单,效率很美好。
0x04 例题gty又虐了一场比赛,被虐的蒟蒻们决定吊打gty。gty见大势不好机智的分出了n个分身,但还是被人多势众的蒟蒻抓住了。蒟蒻们将n个gty吊在n根绳子上,每根绳子穿过天台的一个洞。这n根绳子有一个公共的绳结x。吊好gty后蒟蒻们发现由于每个gty重力不同,绳结x在移动。蒟蒻wangxz脑洞大开的决定计算出x最后停留处的坐标,由于他太弱了决定向你求助。
不计摩擦,不计能量损失,由于gty足够矮所以不会掉到地上。
Input
输入第一行为一个正整数n(1<=n<=10000),表示gty的数目。
接下来n行,每行三个整数xi,yi,wi,表示第i个gty的横坐标,纵坐标和重力。
对于20%的数据,gty排列成一条直线。
对于50%的数据,1<=n<=1000。
对于100%的数据,1<=n<=10000,-100000<=xi,yi<=100000
Output
输出1行两个浮点数(保留到小数点后3位),表示最终x的横、纵坐标。
Sample Input
3
0 0 1
0 2 1
1 1 1
Sample Output
0.577 1.000问题其实就是找一个点
,使得
最小,使用模拟退火算法即可。
0x05 代码
#include #include #include #include
#define INF (1e17)#define EPS (1e-3)#define PI (acos(-1.0))#define FIRE(x) (x *= 0.99)using namespace std;
const int MAXN = 10000 + 10;
int N;
double total = INF;
struct Point {
double x, y, w;
Point (double _x, double _y) : x(_x), y(_y) {}
Point (void) {}
void Read(void) {
scanf("%lf%lf%lf", &x, &y, &w);
}
void operator += (Point t) {
x += t.x; y += t.y;
}
void operator /= (int N) {
x /= N, y /= N;
}
};
Point now, ans, point[MAXN];
inline double Dist(Point a, Point b) {
return sqrt((a.x - b.x) * (a.x - b.x) +
(a.y - b.y) * (a.y - b.y));
}
inline double Statistic(Point p) {
double res = 0.0;
for (int i = 0; i < N; i++) res += Dist(p, point[i]) * point[i].w;
if (res < total) total = res, ans = p;
return res;
}
inline double Rand(void) {
return (rand() % 1000 + 1) / 1000.0;
}
int main(void) {
srand(10086);
scanf("%d", &N);
register int i;
for (i = 0; i < N; i++) point[i].Read(), now += point[i];
now /= N;
double T = 100000.0, alpha, sub;
while (T > EPS) {
alpha = 2.0 * PI * Rand();
Point tmp(now.x + T * cos(alpha), now.y + T * sin(alpha));
sub = Statistic(now) - Statistic(tmp);
if(sub >= 0 || exp(sub / T) >= Rand()) now = tmp;
FIRE(T);
}
T = 0.001;
for (i = 1; i <= 1000; ++i) {
alpha = 2.0 * PI * Rand();
Point tmp(ans.x + T * cos(alpha) * Rand(), ans.y + T * sin(alpha) * Rand());
Statistic(tmp);
}
printf("%.3lf %.3lf\n", ans.x, ans.y);
return 0;
}效率如下图。
为什么效率变化会这么快,其实是伪代码中降温率c取值的原因,降温率越慢,速度越慢(但结果越精确),反之速度越快,但就有可能像那个WA一样降温过快,还没找到最优解温度已经趋于了稳定,所以实际应用时不建议这个值设置得过小。
0x07 完。
但顺便说一句,BZOJ, ZOJ, POJ(此处原文为各大OJ网站,由@dashgua@PeterWang 指出不严谨,谢谢)都不允许使用time()函数(防止出现漏洞),因此用srand()初始化时应该用一个常数。
0x08 其实还没有完
我的专栏说是讲讲算法(实际上也就是讲讲算法),不过是把我自己的学习过程和灵感和大家一起分享,希望大家对算法有新的理解,或者不理解的能够通过我的专栏慢慢理解。所以可能很多内容大家之前已有涉猎。深知这些的ACM大犇就当是看一个ACMer的成长吧。当然,别忘了提出您宝贵的建议和点一个赞喔、
真的完了。