模拟退火 Simulate Anneal 简介

快一年前写的东西了。从洛谷上搬过来滴。

以下是正文。

简介

模拟退火 Simulate Anneal 是一种随机化算法。用于求解方案数量极大(甚至是无穷的)而且不是一个单峰函数的问题。

模拟退火的出发点是基于物理中固体物质的退火过程与一般组合优化问题之间的相似性。模拟退火算法是一种通用的优化算法,其物理退火过程由加温过程、等温过程、冷却过程这三部分组成。

算法思路

对于问题随机出一个新解,如果更优,以 100% 的概率接受;如果不优,以一个与 新解和最优解的差 和 当前时间 相关的概率接受它。

这样执行很多遍,逐渐由局部最优解扩展至全局最优解,答案区间逐步缩小、稳定。

实现过程

降温

模拟退火有三个参数:初始温度 B ,中止温度 E ,降温系数 W

其中, B 为较大数( 1e3,1e4 左右), E 略大于 01e8,1e12 左右), W 略小于 0

开始时使温度 T=BT 不断乘上 W ,直到 T<E 为止。

大致过程如下:

可以看出,随着温度的降低,解逐渐稳定下来,并逐渐集中在最优解附近。

新解

在当前解的基础上生成一个新解,根据题目具体情况选定。一般 T 越大,变化情况越大。当然不方便实现的时候不用这么搞。


如果新解更优,则接受新解;

其他情况:

设原来的最优解为 a ,新解 b ,差 c

显然, T 越高、c 越大时概率大是最合理的。概率一般取

ecx×T

其中 x 为随机数。注意合理选择 x 的范围。一般不需要。

调参

常用的有以下几种:

  • 暴力对拍。

  • 换随机数种子。

  • 多跑几遍模拟退火。形如:while(clock()-t<0.99*CLOCKS_PER_SEC) 不跑到 0.99s 就不停下来。当然 0.99 是卡的太紧了。

例题

P2210 Haywire

模拟退火计算全排列中交换的两个数,每次 O(n) 算出新解,再依据概率决定是否更新即可。模拟退火注意多跑几遍。参数可以使用 B=10000,E=1e12,W=0.99

#define int long long
int a[15][5],b[15],n,nowans=0x3f3f3f3f3f3f3f3f;
const double B=10000,E=1e-12,W=0.999;
//nowans:全局最优解
int f(){//计算需要的干草堆
    int ans=0;
    for(int i=1;i<=n;i++) for(int j=1;j<=3;j++) ans+=abs(b[i]-b[a[i][j]]);
    return ans;
}
void solve(){
    int t=clock();
    while(clock()-t<0.9*CLOCKS_PER_SEC){//能跑多少遍就跑多少遍
        for(double j=B;j>=E;j*=W){
            int x=0,y=0;
            do{
                x=1+rand()%n;
                y=1+rand()%n;
            }while (x==y);//不同的数交换
            swap(b[x],b[y]);//提前交换
            int tmp=f()/2;
            if(tmp<=nowans) nowans=tmp;
            else if(exp((nowans-tmp)*1.0/j)<rand()*1.0/RAND_MAX) swap(b[x],b[y]);
            //如果当前不交换,就把提前交换的换回去
        }
    }
}
signed main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=3;j++) cin>>a[i][j];
        b[i]=i;//初始化全排列
    }
    solve();
    cout<<nowans;
    return 0;
}

P3878 分金币

因为要分成两部分,所以可以划分 a1a(n+1)/2 为一段,剩下为另一段。

与上一题一样,随机交换两个数, O(n) 算新解,再依据概率决定是否更新即可。模拟退火注意多跑几遍。参数可以使用 B=1000,E=1e10,W=0.9 ,每组数据跑 500 遍。

int n,nowans,a[1005];
const double B=1000,E=1e-10,W=0.9;
int f(){//计算当前分办下的金币之差
    int sum1=0,sum2=0;
    for(int i=1;i<=(n+1)/2;i++) sum1+=a[i];
    for(int i=(n+1)/2+1;i<=n;i++) sum2+=a[i];
    return sum1>sum2?sum1-sum2:sum2-sum1;
}
void solve(){
    for(double i=B;i>=E;i*=W){
        int x=0,y=0;
        x=rand()%n+1,y=rand()%n+1;
        swap(a[x],a[y]);//提前交换
        int ans=f();
        if(ans<nowans) nowans=ans;
        else if(exp((nowans-ans)/i)<(double(rand())/RAND_MAX)) swap(a[x],a[y]);
        //如果当前不交换,就把提前交换的换回去
    }
}
signed main(){
    int t;
    cin>>t;
    while(t--){
        cin>>n;
        for(int i=1;i<=n;i++) cin>>a[i];
        nowans=0x3f3f3f3f;
        int tis=500;//运行500遍
        while(tis--) solve();
        cout<<nowans<<'\n';
    }
    return 0;
}
posted @   FReQuenter  阅读(43)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示