• 模拟退火算法


    基本思路

    爬山算法(Hill Climbing):兔子朝着比现在高的地方跳去。它找到了不远处的最高山峰。但是这座山不一定是珠穆朗玛峰。这就是爬山算法,它不能保证局部最优值就是全局最优值。

    模拟退火算法(Simulated Annealing):兔子喝醉了。它随机地跳了很长时间。这期间,它可能走向高处,也可能踏入平地。但是,它渐渐清醒了并朝最高方向跳去。这就是模拟退火。

    玄学算法。

    Hill_Climbing_with_Simulated_Annealing

    模拟退火的主要步骤有几个:

    1. 设置初始温度 (T),初始符合条件的答案;
    2. 通过某种神奇的方式,找到另一个符合条件的新状态;
    3. 分别将两个状态的答案计算出来,并作差得到 (ΔE)
    4. 根据题目要求,贪心的决定是否更换答案(即 选择最优解);
    5. 如果无法替换答案,则根据一定概率替换答案,即运用到平衡概率 (displaystyle e^{frac{ΔE}{T}}) 随机的决定是否替换;
    6. 每一次操作后,进行降温操作,即:将温度 (T) 乘上某一个系数,一般是 0.985−0.999 随具体题目(随缘)定。

    (源:模拟退火 by peng-ym

    const double eps = 1e-15;
    const double t0 = 0.985~0.999;
    
    inline void mnth() { // (需要玄学调试)
        double T = 初始温度;
        while (T>eps) {
            double now = 新状态 如 ans + (rand()*2 - RAND_MAX) * T;
            double delta = f(now) - f(ans);
            if (delta 更优) ans = now;
            else if (exp(±delta / T) * RAND_MAX > rand()) ans = now; // 从局部最优解中跳出
            T *= t0; // 降温
        }
    }
    
    ans = 初始状态;
    mnth();
    print(ans); // 退火后的近似最优解
    

    [JSOI2004] 平衡点

    (n) 个重物,每个重物系在一条足够长的绳子上。每条绳子自上而下穿过桌面上的洞,然后系在一起。记 (X) 处为公共的绳结。假设绳子是完全弹性的(不会造成能量损失),桌子足够高(因而重物不会垂到地上),且忽略所有的摩擦。问绳结 (X) 最终平衡于何处。注意:桌面上的洞都比绳结 (X) 小得多,所以即使某个重物特别重,绳结 (X) 也不可能穿过桌面上的洞掉下来,最多是卡在某个洞口处。

    如题,经过物理学分析,易得本题求解使 (sum m_is_{xi}) 取最小值的 (x) 点位置。然后开始编写程序,然后面向数据编程。(玄学调参)

    /* [JSOI2004] 平衡点
     * Au: GG
     */
    #include <cstdio>
    #include <cmath>
    #include <algorithm>
    #include <ctime>
    using namespace std;
    #define db long double     // double 精度不够,改成 long double
    
    const db eps=1e-15;
    const db t0=0.98;     // 0.99 会 TLE,改成 0.98
    
    int n, px[1003], py[1003], w[1003];
    db ansx, ansy;
    
    inline db dis(db ax, db ay, db bx, db by) {
        return sqrt((ax-bx)*(ax-bx) + (ay-by)*(ay-by));
    }
    
    inline db calc(db x, db y) {
        db res=0.0;
        for (int i=1; i<=n; ++i) res+=dis(x, y, px[i], py[i])*w[i];
        return res;
    }
    
    inline void mnth() {
        db T=200.0;      // 温度调得挺暖和
        while (T>eps) {
            db nowx=ansx + (rand()*2-RAND_MAX) * T;
            db nowy=ansy + (rand()*2-RAND_MAX) * T;
            db delta = calc(nowx, nowy) - calc(ansx, ansy);
            if (delta<0) ansx=nowx, ansy=nowy;
            else if (exp(-delta/T) * RAND_MAX > rand()) ansx=nowx, ansy=nowy;
            T*=t0;
        }
    }
    
    int main() {
        srand(time(0));   // 设定固定种子容易被卡(其实素数 19491001 比较少有人卡吧……)
        scanf("%d", &n);
        for (int i=1; i<=n; ++i)
            scanf("%d%d%d", &px[i], &py[i], &w[i]), ansx+=px[i], ansy+=py[i];
        ansx/=n; ansy/=n;
        mnth();
        printf("%.3Lf %.3Lf
    ", ansx, ansy);
        return 0;
    }
    

    [HAOI2006] 均分数据

    已知 (N) 个正整数:(A_1,A_2,ldots,A_n)。今要将它们分成 (M) 组,使得各组数据的数值和最平均,即各组的数据的数值和均方差最小

    均方差 $displaystyle sigma=sqrt{frac{sum_{i=1}n(x_i-overline{x})2}{n}} $.

    算术平均值 $displaystyle overline{x}=frac{sum_{i=1}^nx_i}{n} $.

    我们可以贪心地把 (N) 个数放入 (M) 组中,即每次放入数值和最小的组。但是这样不一定是最优解。

    模拟退火算法。贪心的顺序很重要,于是不断修改贪心顺序,直到找到近似最优解。

    /* [HAOI2006] 均分数据
     * Au: GG
     */
    #include <cstdio>
    #include <cstring>
    #include <cmath>
    #include <algorithm>
    #include <ctime>
    using namespace std;
    
    const double eps=1e-15;
    const double t0=0.998;
    
    int n, m, p[23], sum[9];
    double ave, ans, now;
    
    inline void calc() {
    	now=0.0, memset(sum, 0, sizeof sum);
    	for (int i=1; i<=n; ++i) {
    		sum[1]+=p[i];
    		sort(sum+1, sum+m+1);
    	}
    	for (int i=1; i<=m; ++i) now+=(sum[i]-ave)*(sum[i]-ave);
    	now=sqrt(now/m);
    }
    
    inline void mnth() {
    	double T=10000.0;   // 高炉炼铁
    	while (T>eps) {
    		int x=rand()%n+1, y=rand()%n+1; while (y==x) y=rand()%n+1;
    		swap(p[x], p[y]); calc();   // 随机替换贪心序列中任意两个元素,构造新状态
    		double delta=now-ans;
    		if (delta<0) ans=now;
    		else if (exp(-delta/T)*RAND_MAX>rand()) ans=now;
    		else swap(p[x], p[y]);  // 交换回来,即不改变状态
    		T*=t0;
    	}
    }
    
    int main() {
    	srand(time(0)), srand(rand());
    	scanf("%d%d", &n, &m);
    	for (int i=1; i<=n; ++i) scanf("%d", &p[i]), ave+=p[i];
    	ave/=m;
    	calc(); ans=now;
    	mnth();
    	printf("%.2lf
    ", ans);
    	return 0;
    }
    

    [USACO2013 Open] Haywire

    Farmer John 有 (N) 只奶牛, ((4 le N le 12), 其中 (N) 是偶数). 他们建立了一套原生的系统,使得奶牛与他的朋友可以通过由干草保护的线路来进行对话交流. 每一头奶牛在这个牧场中正好有 3 个朋友,并且他们必须把自己安排在一排干草堆中. 一条长 (L) 的线路要占用刚好 (N) 堆干草来保护线路. 比如说,如果有两头奶牛分别在草堆 4 与草堆 7 中,并且他们是朋友关系,那么我们就需要用 3 堆干草来建造线路,使他们之间能够联系. 假设每一对作为朋友的奶牛都必须用一条单独的线来连接,并且我们可以随便地改变奶牛的位置,请计算出我们建造线路所需要的最少的干草堆.

    此题也是随机序列的模拟退火。方法同上一题。

    /* Haywire
     * Au: GG
     */
    #include <cstdio>
    #include <cmath>
    #include <algorithm>
    #include <ctime>
    using namespace std;
    
    const double eps=1e-15;
    const double t0=0.998;
    
    int n, G[13][3], f[13], ans, now;
    
    inline int ABS(int x) {return x<0?-x:x; }   // 卡常
    
    inline void calc() {
    	now=0;
    	for (int i=1; i<=n; ++i) for (int j=0; j<3; ++j) 
    		now+= ABS(f[i]-f[G[i][j]]);
    }
    
    inline void mnth() {
    	double T=200.0;   // 数据范围小,不需要高温
    	while (T>eps) {
    		int x=rand()%n+1, y=rand()%n+1; if (x==y) y=rand()%n+1;
    		swap(f[x], f[y]), calc();
    		double delta=now-ans;
    		if (delta<0) ans=now;
    		else if (exp(-delta/T)*RAND_MAX>rand()) ans=now;
    		else swap(f[x], f[y]);
    		T*=t0;
    	}
    }
    
    int main() {
    	srand(time(0)), srand(rand());  // 玄之又玄,众妙之门
    	scanf("%d", &n);
    	for (int i=1; i<=n; ++i) for (int j=0; j<3; ++j) scanf("%d", &G[i][j]);
    	for (int i=1; i<=n; ++i) f[i]=i;
    	calc(); ans=now;
    	mnth();
    	printf("%d
    ", ans/2);
    	return 0;
    }
    

    [NOIP2017TG] 宝藏

    本题标算 状压DP!

    观察题目描述和数据范围,显然,本题可以使用模拟退火算法。(所以是不是所有 状压DP/记忆化搜索 的题目都可以模拟退火乱搞?hhh……)

    贪心地把序列中每一个点连到生成树的代价最小的节点后面,即局部最优解。此时本题转化为序列顺序的问题。模拟退火!

    注意到本人提交此题 9 次才 AC,可见本题调参是比较有代表性的。(上面几题几乎一遍过……)

    /* [NOIP2017TG] 宝藏
     * Au: GG
     */
    #include <cstdio>
    #include <cstring>
    #include <cmath>
    #include <algorithm>
    #include <ctime>
    using namespace std;
    #define inf 0x3f3f3f3f
    
    const double eps=1e-15;
    const double t0=0.998;
    
    int n, m;
    int G[15][15], f[15], dep[15];
    int ans, now;
    
    inline void calc() {  // 贪心方法是关键
    	now=0; memset(dep, 0, sizeof dep);
    	dep[f[1]]=1;
    	for (int i=2; i<=n; ++i) {
    		int w=inf;
    		for (int j=1, ww; j<i; ++j) if (G[f[j]][f[i]]<inf)
    			if ((ww=dep[f[j]]*G[f[j]][f[i]])<w) w=ww, dep[f[i]]=dep[f[j]]+1;
    		if (w<inf) now+=w; else {now=inf; return; }
    	}
    }
    
    inline void mnth() {
    	double T=10000.0;  // 本题对精确度要求比较高,所以要加热到上万度
    	while (T>eps) {
    		int x=rand()%n+1, y=rand()%n+1; if (x==y) y=rand()%n+1;
    		swap(f[x], f[y]); calc();
    		double delta=now-ans;
    		if (delta<0) ans=now;
    		else if (exp(-delta/T)*RAND_MAX > rand()) ans=now;
    		else swap(f[x], f[y]);
    		T*=t0;
    	}
    }
    
    int main() {
    	srand(time(0)), srand(rand()+19260817);  // 迷信一下
    	scanf("%d%d", &n, &m); memset(G, inf, sizeof G);
    	for (int i=1, a, b, c; i<=m; ++i) 
    		scanf("%d%d%d", &a, &b, &c), G[a][b]=G[b][a]=min(G[a][b], c);
    	for (int i=1; i<=n; ++i) f[i]=i;
    	calc(); ans=now;
    	for (int i=1; i<=20; ++i) mnth();  // 连续退火多次保证正确率,当然次数太多也不行,
    	printf("%d
    ", ans);               // 100 次以上会 TLE(因为精度已经挺高了)。
    	return 0;           // 目前此代码跑官方数据用时平均 146ms,极限 297ms,速度刚刚好。
    }
    

    推荐习题:[NOIP2016TG] 愤怒的小鸟(标算 搜索。试试看,你的乱搞能 AC 行吗?)



    拓展

    在不会 TLE 的情况下尽量多地跑 SA:

    我们知道,有一个 clock() 函数,返回程序运行时间。那么这样即可:

    while ((double)clock()/CLOCKS_PER_SEC<MAX_TIME) SA();
    

    其中 MAX_TIME 是一个自定义的略小于 1 的正数,可以取 0.7~0.8。

    M-sea

    例:三角形牧场

    本题标算 DP。

    因为可以贪心,所以可以使用随机算法。

    #include <cstdio>
    #include <cstdlib>
    #include <cmath>
    #include <cstring>
    #include <algorithm>
    #include <ctime>
    using namespace std;
    
    const double eps=1e-10; // 答案只要 2 位精度,所以 eps 可以调小
    const double t0=0.998;
    
    int n, L[43], sum, now, ans;
    
    double heron(int x, int y, int z) {
    	double p=(x+y+z)/2.0;
    	return sqrt(p*(p-x)*(p-y)*(p-z));
    }
    
    inline void calc() { // 贪心
    	register int a=0.0, b=0.0, c=0.0, i=1;
    	for (; i<=n; ++i) if ((a+=L[i])*3.0>=sum) break;
    	for (++i; i<=n; ++i) if ((b+=L[i])*2.0>=sum-a) break;
    	c=sum-a-b;
    	if(a>=b+c||b>=a+c||c>=a+b||a<=0||b<=0||c<=0) now=-1;
    	now=100.0*heron(a, b, c);
    }
    
    inline void mnth() {
    	double T=5000.0;
    	int nocow=ans; // 缓存一下,解决 -1 的问题
    	while (T>eps) {
            int x=rand()%n+1, y=rand()%n+1; while (y==x) y=rand()%n+1;
            swap(L[x], L[y]); calc();
            double delta=now-nocow;
            if (now>0 && delta>=0) nocow=now; // 更优解
            else if (exp(-delta/T)*RAND_MAX>rand()) nocow=now;
            else swap(L[x], L[y]);
            if (ans<nocow) ans=nocow; // 更新答案
            T*=t0;
        }
    }
    
    int main() {
    	srand(time(0)+19260817), srand(rand()^rand());
    	scanf("%d", &n);
    	for (int i=1; i<=n; ++i) scanf("%d", &L[i]), sum+=L[i];
    	random_shuffle(L+1, L+n+1);
    	calc(); ans=now;
    	while ((double)clock()/CLOCKS_PER_SEC<.9) mnth();
    	if (ans==(-2147483648)) printf("-1
    ");
    	else printf("%d
    ", ans);
    	return 0;
    }
    

    玄学算法拓展:

    遗传算法(Genetic):兔子们吃了失忆药片,并被发射到太空,然后随机落到了地球上的某些地方。他们不知道自己的使命是什么。但是,如果你过几年就杀死一部分海拔低的兔子,多产的兔子们自己就会找到珠穆朗玛峰。这就是遗传算法。

    禁忌搜索算法(Tabu Search):兔子们知道一个兔的力量是渺小的。他们互相转告着,哪里的山已经找过,并且找过的每一座山他们都留下一只兔子做记号。他们制定了下一步去哪里寻找的策略。这就是禁忌搜索。

  • 相关阅读:
    Umbraco中更换IndexSet中的NodeType后,搜索页面没有做出对应更改的效果
    Umbraco部署到IIS中权限问题(back office没有权限新建template)
    C控制台密码输入:输入一个字符显示一个星号
    C项目实践--家庭财务管理系统
    C 编程中fseek、ftell的用法总结
    C ++模板的声明和实现为何要放在头文件中?
    头文件与cpp文件为什么要分开写
    printf、sprintf与fprintf 的用法区分
    C编程中fread 、fwrite 用法总结
    C从控制台(stdin)输入带空格的字符串到字符数组中
  • 原文地址:https://www.cnblogs.com/greyqz/p/9911980.html
Copyright © 2020-2023  润新知