浅谈随机化与模拟退火

浅谈随机化与模拟退火

前言

模拟赛时朋友经常用随机化乱搞,导致最后一道黑被他骗了 40 p t s 40pts 40pts,而我拼尽全力只有 10 p t s 10pts 10pts,不过也是我太菜了。

后来回家后研究了几天的随机化算法,本文是总结。

随机化算法如果能用上非常好用,几乎无脑敲代码,甚至可以轻松 A 掉难题。

随机数

随机是随机化的基础。即获取随机数。

这个比较简单,不想讲。

注意开始时随机下种子。

随机算法

随机算法的特点是玄学。一般用来打暴力骗分,或者用一些随机算法比如模拟退火搞正解。

随机算法可以分为以下步骤:

  • 随机分配元素

比如打乱一个数列的顺序、每个元素分配集合、给元素赋值等。

即答案跟什么有关。

  • 计算结果

字面意思,将随机后的元素状态进行计算。

  • 与历史最优值比较

字面意思,把随机后的结果与最优值比较,取最优。

正确概率

这个在做题时一般不用算,因为算了不会优化也没用。

我们设随机的总情况有 S S S 种,能使答案正确的情况有 A A A 种,我们能够随机 T T T 次。

那么显然正确的概率就是 ( 1 − ( S − A S ) T ) × 100 % (1-(\frac{S-A}{S})^T)\times 100\% (1−(SS−A)T)×100%。

注意概率不是 A T S \frac{AT}{S} SAT,至于为什么:

"一发导弹拦截率是 70 % 70\% 70%,我一下子打 3 3 3 发,拦截率就是 210 % 210\% 210%!"

随机算法的优化方法

随机算法本质是舍弃了正确性换时间复杂度。所以优化可以往时间和正确性想。

但正确性不怎么常用,而且每题都不一定一样。

优化时间

在随机过程中尽量避免无意义情况。

如果计算的时候有式子,可以化简一下,维护尽量少的元素得出答案

剪枝。这一技巧对于数据水非常好用。有时甚至可以暴力剪枝当正解。

当你优化了每次流程的时间,就可以换来更多次的随机,正确性也有所提升。

例题

洛谷 B3800 弹幕\](\[B3800 [NICA #1\] 弹幕 - 洛谷](https://www.luogu.com.cn/problem/B3800))。 这题可以更好理解如何算正确概率。 对于一个一元三次方程,它的解最多就是三个,而所有解最多 6 × 10 5 6\\times 10\^5 6×105,多随机几次一定是可以随机到的。 代码: ```cpp #include using namespace std; typedef long long ljl; inline void Rd(auto &num); const int N=2e5+5,M=1e6+5; const ljl Mod=1e6+1; int n; bool vis[M]; struct NODE{ ljl a,b,c; }node[N]; bool check(ljl x) { for(int i=1;i<=n;++i) { if(x*x*x+node[i].a*x*x+node[i].b*x+node[i].c==0) return 0; } return 1; } int main(){ int ___=rand()%20120110; srand(___); Rd(n);ljl x=0; for(int i=1;i<=n;++i){ Rd(node[i].a);Rd(node[i].b);Rd(node[i].c);} while(!check(x)) { while(vis[x])x=1ll*rand()*rand()*rand()%Mod; vis[x]=1; x=1ll*rand()*rand()%Mod; } printf("%lld\n",x); return 0; } inline void Rd(auto &num) { num=0;char ch=getchar();bool f=0; while(ch<'0'||ch>'9') { if(ch=='-')f=1; ch=getchar(); } while(ch>='0'&&ch<='9') { num=(num<<1)+(num<<3)+(ch-'0'); ch=getchar(); } if(f)num=-num; return; } ``` [\[USACO13OPEN\] Haywire B - 洛谷](https://www.luogu.com.cn/problem/P2210)。 本题正解是状压 dp,但是显然这么做非常复杂。 考虑随机化算法。 注意到如果我们有了奶牛的排列,就可以 O ( n ) O(n) O(n) 算出答案。 所以我们可以给奶牛们随机排位置。 排完后计算答案,取最优值。 代码比较简单,就放下计算与随机的代码。 计算: ```cpp int calc() { int ans=0; for(int i=1;i<=n;++i) for(int j=1;j<=3;++j) ans=ans+abs(a[i]-a[p[i][j]]); return ans/2; } ``` 随机: ```cpp for(int i=1;i<=n;++i) swap(a[i],a[rand()%n+1]);//每个奶牛的位置 ``` 这里也是一种对于数列随机打乱的模板。 #### 爬山算法与模拟退火 ##### 爬山算法 爬山算法是一种类似于贪心的局部最优算法,类似于 dfs。 我们可以把所有可能的状态设为 x x x 轴坐标(有可能状态数量是无穷的),对应的答案设为 y y y 轴坐标。 那么我们的最优解就是这个函数的峰顶。 爬山算法的思想就是一开始随机个状态,然后在附近随机搜寻状态,如果更优则采纳,否则跳过。 显然爬山算法容易陷入局部最优解。因为如果全局最优与初始状态相距太远,就基本不会被爬山算法找到。 ##### 模拟退火 > 退火是一种金属热处理工艺,指的是将金属缓慢加热到一定温度,保持足够时间,然后以适宜速度冷却.目的是降低硬度,改善切削加工性;消除残余应力,稳定尺寸,减少变形与裂纹倾向;细化晶粒,调整组织,消除组织缺陷.准确的说,退火是一种对材料的热处理工艺,包括金属材料、非金属材料.而且新材料的退火目的也与传统金属退火存在异同.------百度百科 模拟退火与爬山算法的重要区别是它对待非更优状态不是否定,而是有概率接受。且这个概率随着时间缓慢降低。 假设我们现在的状态是 S S S,新状态是 S ′ S' S′,概率系数为 T T T,两个状态的差值为 D D D。需要保证 D ≥ 0 D\\ge 0 D≥0。 * S ′ S' S′ 比 S S S 优:直接接受。 * 以 e − D T e\^{\\frac{-D}{T}} eT−D 的概率接受。 概率系数 T T T 又叫做温度。是为了模拟金属退火时的降温过程。 如何降温? 我们再制定一个降温系数 T D TD TD,每次搜寻状态后 T ← T × T D T\\leftarrow T\\times TD T←T×TD,这样就可以让接收状态的概率越来越小,对应退火过程中分子运动越来越稳定。 而在平时做题的过程中,也可以多次退火取最优值以增加正确率。 看些例题。 [\[TJOI2010\] 分金币 - 洛谷](https://www.luogu.com.cn/problem/P3878)。 如何使用模拟退火? 首先不难想到将所有金币分为两个集合,大小相差不超过 1 1 1。 我们设状态序列为 a a a。 a i a_i ai 表示第 i i i 个金币的价值。 每次搜寻新状态就是随机交换两个 a i a_i ai 与 a j a_j aj 的值。 代码: ```cpp #include using namespace std; typedef long long ljl; inline void Rd(auto &num); #define db double const db Td=0.996; const int N=35; const ljl inf=1e18; int n,T; ljl a[N],ans; db Rand(){return (db)rand()/RAND_MAX;}//随机取小数 ljl calc() { ljl ans=0; for(int i=1;i<=n/2;++i)ans+=a[i]; for(int i=n/2+1;i<=n;++i)ans-=a[i]; return abs(ans); } void Main() { Rd(n);ans=inf; for(int i=1;i<=n;++i) Rd(a[i]); for(int _=1;_<=50;++_) { for(db t=5000.0;t>1e-10;t*=Td) { int x=rand()%n+1,y=rand()%n+1; swap(a[x],a[y]);//随即交换 ljl nxt=calc(); db delt=(db)nxt-(db)ans;//delt>=0 if(ans>nxt)ans=nxt; if(exp(-delt/t)>Rand())continue;//概率达到了,接受新状态 else//否则不接受新状态,换回原来的 swap(a[x],a[y]); } } printf("%lld\n",ans); return; } int main(){ int ___=rand()%20120110; srand(___); Rd(T); while(T--)Main(); return 0; } inline void Rd(auto &num) { num=0;char ch=getchar();bool f=0; while(ch<'0'||ch>'9') { if(ch=='-')f=1; ch=getchar(); } while(ch>='0'&&ch<='9') { num=(num<<1)+(num<<3)+(ch-'0'); ch=getchar(); } if(f)num=-num; return; } ``` [P1559 运动员最佳匹配问题 - 洛谷](https://www.luogu.com.cn/problem/P1559)。 也和上一题基本一样。注释在代码里。 ```cpp #include using namespace std; typedef long long ljl; inline void Rd(auto &num); #define db double const int N=25; const db TD=0.9983; db Rand(){return (db)rand()/RAND_MAX;} int n; ljl p[N][N],q[N][N],ans,a[N]; ljl calc() { ljl ans=0; for(int i=1;i<=n;++i) ans+=p[i][a[i]]*q[a[i]][i]; return ans; } void SA() { // for(int i=1;i<=n;++i)a[i]=i; for(int i=1;i<=n;++i)swap(a[i],a[rand()%n+1]); for(db T=5000.0;T>1e-10;T*=TD) { int idx=rand()%n+1,t=rand()%n+1,y=0;//将idx号男与t号女配对 for(int i=1;i<=n;++i) { if(a[i]==t) { a[i]=a[idx];y=i; break; } } a[idx]=t; ljl nxt=calc(); if(nxt>ans){ans=nxt;continue;} ljl delt=ans-nxt;//delt>=0 if(exp(-delt/T)>Rand())continue; a[idx]=a[y];a[y]=t; } return; } int main(){ srand(rand()); Rd(n); for(int i=1;i<=n;++i) for(int j=1;j<=n;++j) Rd(p[i][j]); for(int i=1;i<=n;++i) for(int j=1;j<=n;++j) Rd(q[i][j]); for(int i=1;i<=n;++i)a[i]=i; for(int _=1;_<=600;++_) SA(); printf("%lld\n",max(0ll,ans)); return 0; } inline void Rd(auto &num) { num=0;char ch=getchar();bool f=0; while(ch<'0'||ch>'9') { if(ch=='-')f=1; ch=getchar(); } while(ch>='0'&&ch<='9') { num=(num<<1)+(num<<3)+(ch-'0'); ch=getchar(); } if(f)num=-num; return; } ``` 所以说,模拟退火非常好用,把模板理解或背下来后,随便套题目。 > 模拟退火是对一道好题的不敬,但如果我又不用动脑又可以拿分我会更尊敬此题。

相关推荐
Felven1 小时前
A. Add and Divide
数据结构·算法
Frostnova丶2 小时前
LeetCode 67. 二进制求和
算法·leetcode
上海锟联科技2 小时前
DAS 与 FBG 振动监测对比:工程应用中该如何选择?
数据结构·算法·分布式光纤传感
星火开发设计2 小时前
模板参数:类型参数与非类型参数的区别
java·开发语言·前端·数据库·c++·算法
白太岁2 小时前
操作系统开发:(9) 从硬件复位到程序执行:如何编写符合硬件动作的启动文件与链接脚本
c语言·汇编·嵌入式硬件·系统架构
张3蜂2 小时前
Python pip 命令完全指南:从入门到精通
人工智能·python·pip
JialBro2 小时前
【嵌入式】直流无刷电机FOC控制算法全解析
算法·嵌入式·直流·foc·新手·控制算法·无刷电机
昌兵鼠鼠2 小时前
LeetCode Hot100 哈希
学习·算法·leetcode·哈希算法
忘梓.2 小时前
二叉搜索树·极速分拣篇」:用C++怒肝《双截棍》分拣算法,暴打节点删除Boss战!
开发语言·c++·算法