随机算法与卡时
卡时
首先膜拜吴爷在模拟赛中用卡时取得了暴打我的成绩。
卡时是防 TLE 的美妙方法,具体代码就一行,核心思想就是只要不超时就一直跑,跑出啥算啥,结合后面的随机算法可以加大乱搞出正解的概率。
代码:
#include<ctime>
while((double)clock()/CLOCKS_PER_SEC<MAXTIME)solve();
2024/06/01 我在儿童节模拟赛中运用卡时+乱搞的方法切掉了 A 题,这反映了卡时算法在脚造数据中之好使。
爬山算法
爬山算法是一种局部择优的方法,采用启发式方法,是对深度优先搜索的一种改进,它利用反馈信息帮助生成解的决策。
直白地讲,就是当目前无法直接到达最优解,但是可以判断两个解哪个更优的时候,根据一些反馈信息生成一个新的可能解。
因此,爬山算法每次在当前找到的最优方案 \(x\) 附近寻找一个新方案。如果这个新的解 \(x'\) 更优,那么转移到 \(x'\) ,否则不变。
这种算法对于单峰函数显然可行。那多峰怎么办?
法一:我搞好几个瞎子,每个跑一遍,就行了。这种方法升升级就叫遗传算法,但我暂时学不会,学会了再来补充(大概是不会补了,但大抵会另写一篇)。
法二:
模拟退火
“退火”是一种金属热处理工艺,指的是将金属缓慢加热到一定温度,保持足够时间,然后以适宜速度冷却。(来自百度百科)模拟退火就是模拟了这个过程。
用一句话来说,模拟退火的灵魂就是随一个新状态,如果新状态好,毫不犹豫跳过去,这里先不管他;否则的话有一定概率(记作 \(p\))跳过去(这也是对爬山算法的升级,避免在一座小山上走死的困境),越是靠近开始,概率越大,越是将近结束,概率越小。这一点的灵感同样来自打铁。打铁的时候先要把铁块烧到一个极高的温度,然后不断冷却,在冷却的过程中敲打铁块,让铁块成型,我们知道,时间越早,铁块越容易变形;时间越晚,铁块越不容易变形,我们尽量在前半段成型,后半段修修补补就行,模拟退火也是这样。
接下来讨论如何通过控制 \(p\) 来实现我们上面说的过程。我们模拟退火的温度,设温度是 \(T\)。不妨设能量高,答案优。设新状态与旧状态能量之差为 \(\Delta E\),若 \(\Delta E >0\),无脑跳;否则,跳过去的概率 \(p=e^{\frac{\Delta E}{T}}\)。为了结果更精确,我们维护的不是最终值,而是过程中的最小值。如果 \(p\) 小于 \(0\) d到 \(1\) 之间随的一个数,就让这个点跳过去。
为什么是 \(e^x\) 这样的东西?我们可以动用科技手段画出函数 \(f(x)=e^x\) 的图像,可以观察到 \(f(x)\) 在值域内单调递增,\(f(0)=1\),且对于任意的 \(x\) ,\(f(x)>0\),因此当 \(\Delta E <0\) 时, \(f(\frac{\Delta E}{T}) \in (0,1]\)。显然,这个东西还是越小越好,所以是小于某个数。
例题
平面内撒几个点,求最小的圆能正好包住这些点。
sol:我们明确,在知道圆心的条件下,可以求出圆的半径,因此我们只需要对圆心模拟退火,每次让圆心向随机的方向走随机的长度,判断走不走。代码就不放了,你往下看看就有类似的。
升级版:平面内扔几条线段,求最小的原与这些线段都有交点(这里原是指圆内所有点与圆上所有点构成的集合,没打错)。这个有Luogu link和AtCoder link。
用向量法(或者解析式法,但精度是个问题,会挂)求出距离,就可以转化成上面的典了。
代码:
#include<iostream>
#include<iomanip>
#include<cstdlib>
#include<cstdio>
#include<ctime>
#include<cmath>
using namespace std;
int n;
double ans,ansx,ansy;
struct point{
double x,y;
inline void read(){scanf("%lf%lf",&x,&y);}
inline double len(){return sqrt(x*x+y*y);}
}a[105],b[105];
point operator -(point a,point b){return {a.x-b.x,a.y-b.y};}
point operator +(point a,point b){return {a.x+b.x,a.y+b.y};}
inline double dot(point a,point b){return a.x*b.x+a.y*b.y;}
inline double cross(point a,point b){return a.x*b.y-b.x*a.y;}
inline double Rand(){return (double)rand()/RAND_MAX;}
inline double dis(point p,point a,point b)
{
point ab=b-a,ap=p-a,bp=p-b;
if(dot(ab,ap)<0)return ap.len();
else if(dot(ab,bp)>0)return bp.len();
else return fabs(cross(ab,ap)/ab.len());
}
inline double calc(double x,double y)
{
double res=-1e18;
for(int i=1;i<=n;i++)res=max(res,dis({x,y},a[i],b[i]));
if(res<ans)ans=res,ansx=x,ansy=y;
return res;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
a[i].read(),b[i].read();
ansx+=(a[i].x+b[i].x)/2;
ansy+=(a[i].y+b[i].y)/2;
}
ansx/=n,ansy/=n;
ans=calc(ansx,ansy);
while((double)clock()/CLOCKS_PER_SEC<=1.95)//MAX_TIME不要卡的太满
{
double T=1e3,nx=ansx,ny=ansy;
//nx,ny的作用是让下一次从当前最优开始跑
while(T>1e-7)
{
double x=nx+T*(Rand()*2-1);
double y=ny+T*(Rand()*2-1);
if(exp((calc(nx,ny)-calc(x,y))/T)>Rand())nx=x,ny=y;
T*=0.996;
}
}
cout<<fixed<<setprecision(30)<<ans<<"\n";
return 0;
}
最后,只要你注意力足够好,你还可以用模拟退火 A 掉 这个题 ;即使你注意力不好,你也可以获得 72 分的好成绩(不如总司令)。