学习笔记—— % 你 退 货

发布时间 2023-10-06 16:42:46作者: Spade-A

最近对人类智慧比较感兴趣,于是学了一下这之中臭名昭著比较有名的 %你退货 模拟退火.


看不懂的定义

模拟退火算法来源于固体退火原理,

是一种基于概率的算法,将固体加温至充分高,再让其徐徐冷却,

加温时,固体内部粒子随温升变为无序状,内能增大,

而徐徐冷却时粒子渐趋有序,在每个温度都达到平衡态,最后在常温时达到基态,内能减为最小.

(别猜了就是从百度粘的)

(看个乐呵就行,没用)


个人理解

百度百科写的很反人类,但实际上这个算法非常亲民(除了调参的时候).

实际就是一种受自然现象启发发明的效率很高的随机化算法.

(像这样的算法还有很多,包括但不限于遗传算法)


大致过程

我们可以认为是在一条函数图像(一般是多峰)上有一定策略地跳来跳去.

为了便于理解,我们接下来会把这个函数图像比作一座山.

首先我们需要随便确定一个起始点(一般是选择原始数据的平均数),

然后就开始退火.

一般能模拟退火的题都会有两种东西:

一种是 '状态' ,也就是山上的点.

一种是 '结果' ,也就是当前点所在的"高度".

我们会基于当前状态点随机的跳来跳去.

对于更优的点,我们一定会接受.

而对于不更优的点呢?我们 概率接受 它.

为什么不是一定不接受?

想一个很简单的问题:高度比较高的地方就一定离山顶近吗?

显然不.

所以相比于完全贪心的爬山算法,模拟退火最大的特点就在此:

我们会以一定概率接受一个比当前状态不更优的点.

这个概率函数为

\[e^{-\Delta/T} \]


参数

模拟退火的框架大概是这样的:

const ldb down=0.996;
const ldb edge=RAND_MAX;
#define ldb long double
//请务必注意你变量的类型
//作者曾不止一次因为ldb写成int调了半天

void SA()
{
    ldb T=3000;//初始温度
    while(T>=1e-15)
    {
        int ex=x+rnd();// 随机化得到新状态
        int eans=energy(ex);//求这个新状态对应的结果

        int D=eans-ans;//新结果与原结果的差值

        if(D<0)
            x=ex,ans=eans;//新结果更优,接受
        else if(exp(-D/T)*edge>rnd())
            x=ex;//否则一定概率接受这个状态

        T*=down;//降温
    }
}

可以发现里面有很多参数,包括但不限于

\(T\)(初始温度),\(1e-15\)(终止温度),\(down\)(退火速度).

在实际的题目中,我们会不断地改变他们的值以控制运行时间或是提高正确率.

这个过程叫调参,可以认为是整个模拟退火中最麻烦的一部分.


一 些 例 题

P1337 [JSOI2004] 平衡点 / 吊打XXX

著名的模拟退火板子题.枚举的是重心的位置.

根据一些除了作者人人都会的物理知识,由重心产生的答案是最小的.

#include<bits/stdc++.h>
using namespace std;
#define il inline
#define endl '\n'
const int N=1017000;
const int edge=RAND_MAX;
#define ldb long double
#define Croll(i,l,r) for(int i=l;i<=r;++i)
#define Troll(i,l,r) for(int i=l;i>=r;--i)
//////////
const ldb down=0.996;

int n;
struct starfield
{int x,y,w;}o[N];

void SA();

ldb ansx,ansy,answ;
ldb heart(ldb,ldb);
//////////
il int read();
//////////
int main()
{
    srand(time(0));

    n=read();
    Croll(i,1,n)
    {
        o[i]={read(),read(),read()};
        ansx+=o[i].x,ansy+=o[i].y;
    }
    ansx/=n;ansy/=n;
    answ=heart(ansx,ansy);

    SA();SA();SA();SA();
    SA();SA();SA();SA();
    SA();SA();SA();SA();
    SA();SA();SA();SA();
    //矩阵加速()

    printf("%.3Lf %.3Lf",ansx,ansy);

    return 0;
}
//////////
void SA()
{
    ldb T=3000;
    while(T>1e-15)
    {
        ldb ex=ansx+(rand()*2-edge)*T;
        ldb ey=ansy+(rand()*2-edge)*T;
        // cout<<ex<<" "<<ey<<endl;
        if(ex>10000||ey>10000||ex<-10000||ey<-10000)
        {
            T*=down;
            continue;
        }
        ldb ew=heart(ex,ey);
        ldb De=ew-answ;
        if(De<0)
        {
            ansx=ex;ansy=ey;
            answ=ew;
        }
        else if(exp(-De/T)*edge>rand())
        {
            ansx=ex;ansy=ey;
        }
        T*=down;
    }
}

ldb heart(ldb x,ldb y)
{
    ldb ans=0,dx,dy;
    Croll(i,1,n)
    {
        dx=x-o[i].x;
        dy=y-o[i].y;
        ans+=sqrt(dx*dx+dy*dy)*o[i].w;
    }
    return ans;
}
//////////
il int read()
{
    int f=1,x=0;char w=getchar();
    while(w<'0'||w>'9')
    {if(w=='-')f=-1;w=getchar();}
    while(w>='0'&&w<='9')
    {x=(x<<3)+(x<<1)+(w^48);w=getchar();}
    return f*x;
}

P2210 Haywire

个人感觉这道题比较有代表性.

这道题代表了模拟退火能处理的第二类问题:
通过改变序列元素的顺序得到答案.

#include<bits/stdc++.h>
using namespace std;
#define il inline
#define endl '\n'
const int N=1017;
#define lol long long
#define ldb long double
#define Croll(i,l,r) for(int i=l;i<=r;++i)
#define Troll(i,l,r) for(int i=l;i>=r;--i)
//////////
#define fr first
#define sc second
#define PII pair <int,int>

const ldb down=0.996;
const int edge=RAND_MAX;
mt19937 rnd(19280817);
//////////
int n,tot;
PII frd[N];

int pos[N];
void SA();

int ans;
int energy();
//////////
il int read();
//////////
int main()
{
    n=read();
    Croll(i,1,n)
    {
        int f1=read(),f2=read(),f3=read();
        if(f1>i)
            frd[++tot]={i,f1};
        if(f2>i)
            frd[++tot]={i,f2};
        if(f3>i)
            frd[++tot]={i,f3};
    }
    Croll(i,1,n)
        pos[i]=i;
    ans=energy();

    SA();SA();SA();SA();
    SA();SA();SA();SA();
    SA();SA();SA();SA();
    SA();SA();SA();SA();

    cout<<ans<<endl;

    return 0;
}
//////////
void SA()
{
    ldb T=3000;
    while(T>1e-15)
    {
        int rx=rnd()%n+1,ry=rnd()%n+1;
        swap(pos[rx],pos[ry]);

        int e=energy();
        int D=e-ans;
        if(D<0)
            ans=e;
        else if(exp(-D/T)*edge<rnd())
            swap(pos[rx],pos[ry]);
			//注意:这里的意思是,1-exp(-D/T)的概率变回原状态,等价于exp(-D/T)的概率接受当前状态
        
        T*=down;
    }
}

int energy()
{
    int dis=0;
    Croll(i,1,tot)
    {
        int x=frd[i].fr,y=frd[i].sc;
        dis+=abs(pos[x]-pos[y]);
    }
    return dis;
}
//////////
il int read()
{
    int f=1,x=0;char w=getchar();
    while(w<'0'||w>'9')
    {if(w=='-')f=-1;w=getchar();}
    while(w>='0'&&w<='9')
    {x=(x<<3)+(x<<1)+(w^48);w=getchar();}
    return f*x;
}