前几天心血来潮,就搞了一张任务清单(淦!还不是因为我找不到$lin $ _ $ toto$的“洛咕超级任务清单” $ js $ 脚本了)然后脑子一抽,就把模拟退火工工整整的写上去了,学了好久看了好多外国的视频才懂,我太南了。
一道小题目
如图:有\(n\)个重物,每个重物系在一条足够长的绳子上。每条绳子自上而下穿过桌面上的洞,然后系在一起。图中\(X\)处就是公共的绳结。假设绳子是完全弹性的(不会造成能量损失),桌子足够高(因而重物不会垂到地上),且忽略所有的摩擦。
问绳结X最终平衡于何处。
注意:桌面上的洞都比绳结\(X\)小得多,所以即使某个重物特别重,绳结\(X\)也不可能穿过桌面上的洞掉下来,最多是卡在某个洞口处。
输入格式
文件的第一行为一个正整数\(n\)\((1≤n≤1000)\),表示重物和洞的数目。接下来的\(n\)行,每行是3个整数:\(Xi.Yi.Wi\),分别表示第i个洞的坐标以及第 \(i\)个重物的重量。$(-10000≤x,y≤10000, 0<w≤1000 ) $
输出格式
你的程序必须输出两个浮点数(保留小数点后三位),分别表示处于最终平衡状态时绳结X的横坐标和纵坐标。两个数以一个空格隔开。
什么 事 模拟退火啊
事实证明看百度百科是会看傻的,\(OI\)的退火和分子热运动没多大关系。
小思考问题
我们知道了全国某几个城市的坐标,求一条路线使得从北京出发经过所有城市的路径最短且每条路只走一次
这道题要是直接暴搜的话……
如果有25座城市,而且你的\(PC\)强大到一秒1e8次运算,宁得日夜不停的算大概四十九亿年,可怕吗
但是用模拟退火,可以飞快地算出答案。
模拟退火是用来求多峰函数的极值的 (尽管通常并不会将多峰函数体现出来,需要宁自己想),设置合适的参数可以在很快的速度里求得近似的最优解,所以说这个算法不一定得满分,但是可以很容易的得一个高分。
在此以做上边题的亲身经历说明一下
(
先说几个概念
\(T\) : 初始温度,是一个自己看心情和题目设定的量,这个数越大,正解概率越高,但是耗时越长。
\(down\) : 降温系数,设置这个系数的目的是让\(T\)能以一个合适的速度减小,使得当\(T\)在到达边界时我们已经尝试了足够多组解来使最终解接近最优解。指数函数的增长速度是极其迅速的,所以我们可以给\(down\)附一个很小的值,通常在\(0.98 - 0.995\)
之间酌情瞎搞,越大约不准确,但越快。
\(exp( n )\) :c++一个函数,计算\(e ^ n\),其中\(e\)为自然对数。
\(de\) : 这次得到的解和之前求到的最优解的差值,有点类似于化学里的熵变,用来评价这次的解是否更优。
\(RAND\)_ \(MAX\) :c++能生成的最大的随机数,即32767。
\(rand()\) :c++函数,生成随机数。
怎么退火呢?
举个烂大街的栗子:
贪心算法:兔子朝着比现在高的地方跳去。它找到了不远处的最高山峰。但是这座山不一定是珠穆朗玛峰。这就是贪心,它不能保证局部最优值就是全局最优值。
模拟退火:兔子喝醉了。它随机地跳了很长时间。这期间,它可能走向高处,也可能踏入平地。但是,它渐渐清醒了并朝最高方向跳去。这就是模拟退火。
一张图骗:
可以看到它是逐渐趋近于最优解的
本题为例的伪代码:
int T = 233;
int tempx, tempy, tempans, de;
//下面的x, y, ans为临时最优解
while( T > 1e-15 ){ //T十分接近零
tempx = x + (rand() * 2 - RAND_MAX) * T;
tempy = y + (rand() * 2 - RAND_MAX) * T;
//以本题为例,rand() * 2 - RAND_MAX是一个小套路,这样可以生成从 -32767 到 32767 的随机数,以让坐标点转移到一个随机位置,随着不断降温,T会越来越小,所以坐标点转移的范围也会越来越小,逐渐逼近最优点
tempans = solve( tempx, tempy )
//solve是你用来求距离的函数
de = tempans - ans;
if( de < 0 ){//是大于还是小于具体问题判断
x = tempx;
y = tempy;
w = tempw;
} else if( exp( -de / T ) * RAND_MAX > rand() ){//以一个概率选择是否接受,但是就算是接受,我们也不会用它来更新最优解,因为我们当前的最优解比它优秀。de的正负性具体题目具体分析,也可以两个都试一下,看哪个输出正确样例
x = tempx;
y = tempy;
}
T *= down;//降温
}
题目分析
我最开始想的是受力分析然后搞个合力,最后物体在合力方向上移动,但是一想,合力方向似乎会变,然后就没想法了(俺太弱了\(QWQ\)
看讨论区有人说用质心公式搞???我不太懂,没搞。
最后用模拟退火瞎搞出来的,看题解还有大佬用凸包什么的搞出来了,反正我不会。
然后就模拟退火搞呗,考虑到重力势能是趋近于系统重力势能最小的,所以当绳子的结点静止时,重力势能之和最小,那么我们可以求出在每个点时的重力势能大小,以此来比较当前解和最优解。
AC代码:
#include<bits/stdc++.h>
using namespace std;
#define rint register int
#define down //请自己尝试
int de;
int n;
double ansx, ansy, answ;
struct node{
int x, y, w;
}a[1050];
inline int read( void ){
int re = 0, f = 1;
char ch = getchar();
while( ch > '9' || ch < '0' ){
if( ch == '-' ) f = -1;
ch = getchar();
}
while( ch >= '0' && ch <= '9' ){
re = re * 10 + ch - '0';
ch = getchar();
}
return re * f;
}
inline double init( double x, double y ){
double re = 0.0, d1, d2;
for( rint i = 1; i <= n; i++ ){
d1 = x - a[i].x, d2 = y - a[i].y;
re += sqrt( d1 * d1 + d2 * d2 ) * a[i].w;
}
return re;
}
inline void SA(){
double T = //请自己尝试
double de, tw, tx, ty;
while( T > 1e-15 ){
//cout << T << endl;
tx = ansx + ( rand() * 2 - RAND_MAX ) * T;
ty = ansy + ( rand() * 2 - RAND_MAX ) * T;
tw = init( tx, ty );
de = tw - answ;
if( de < 0 ){
ansx = tx;
ansy = ty;
answ = tw;
} else if( exp( -de / T ) * RAND_MAX > rand() ){
ansx = tx;
ansy = ty;
}
T *= down;
}
}
int main( void ){
n = read();
for( rint i = 1; i <= n; i++ ){
a[i].x = read(); a[i].y = read(); a[i].w = read();
ansx += a[i].x, ansy += a[i].y;
}
ansx /= n, ansy /= n;
answ = init( ansx, ansy );
//cout << ansx << ' ' << ansy;
SA();
SA();
SA();
SA();
SA();
printf( "%.3lf %.3lf", ansx, ansy );
return 0;
}