news 2026/9/13 17:18:30

OI-wiki 爬山算法完全指南:原理、实现、例题与调参实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OI-wiki 爬山算法完全指南:原理、实现、例题与调参实战

OI-wiki 爬山算法完全指南:原理、实现、例题与调参实战

【免费下载链接】OI-wiki:star2: Wiki of OI / ICPC for everyone. (某大型游戏线上攻略,内含炫酷算术魔法)项目地址: https://gitcode.com/GitHub_Trending/oi/OI-wiki

导读

爬山算法(Hill Climbing)是 OI / ICPC 竞赛中一种简单而暴力的局部择优方法:它利用目标函数的反馈信息,在当前最优解的邻域内不断生成更优候选解。本文以 OI-wiki 的 hill-climbing.md 为主体,结合仓库内 hill-climbing_1.cpp、hill-climbing_2.cpp 两份可编译参考代码及其配套测试数据,系统讲解算法思想、温度降温机制、两道经典例题(JSOI2008 球形空间产生器、BZOJ 3680 吊打 XXX)的完整求解流程,并给出多次爬山优化与参数调优的实战建议。读完本文,你将能够在无法写出严格正解的计算几何/数学题中,用爬山算法快速逼近最优解,并理解它为何最终需要与模拟退火配合使用。


一、算法思想:一种局部择优的启发式搜索

爬山算法是深度优先搜索的一种改进,其核心是"用反馈信息帮助生成解的决策"。它解决的问题场景是:当前无法直接推导出最优解,但可以判断两个解中哪个更优。此时算法利用这一比较能力,根据反馈信息不断生成新的可能解。

用一句话概括整个迭代过程:

每次在当前找到的最优方案 $x$ 附近寻找一个新方案 $x'$;如果 $x'$ 更优,就转移到 $x'$,否则保持不变。

这个过程天然要求目标函数具备一定的"连续向好"性质,因此对于单峰函数,爬山算法显然可行——它总能沿着坡面一路爬向唯一的峰顶。

为什么不用三分?

一个很自然的疑问是:既然单峰函数可以直接三分(ternary search),为什么还需要爬山?

原文档给出了两个关键理由,这正是爬山算法的价值所在:

  1. 正解写法不易掌握:常见于"毒瘤"计算几何题与数学题,即使知道函数单峰,也难以写出解析式或严谨的二分判定条件;
  2. 状态维度过多:当问题本身维度很多时,难以容易地写出分治算法(例如下文的例 1 本可以用二分完成合法正解,但高维球心的形式使二分实现繁琐),此时可以通过非常暴力的计算得到最优解。

致命缺陷:陷入局部最优

爬山算法的贪心本质决定了它只看"眼前更优的方向"。对于多峰函数,算法很容易停在某个局部最优解上:它认为周围没有比当前更好的点,于是驻足,即便远处存在更高的山峰。

下图直观展示了这一情形:绿色箭头是全局最优解,而红色箭头是爬山算法可能找到并停驻的局部最优解。

在 docs/misc/images/hill-climbing.png 对应的插图中可以看到,只要起点位于错误的"山坡",贪心上升就会把算法带入谷顶的局部峰。这也是本文最后引入模拟退火的原因。


二、具体实现:温度参数与降温机制

爬山算法的工程实现有一个重要细节:引入温度参数(与模拟退火类似,但语义不同——爬山不包含"接受劣解"的随机跳变)。

直觉类比:醉酒爬山的兔子

原文档给出了一个经典类比:爬山算法就像一只喝醉了的兔子在山上跳。

  • 它每次都朝着它所认为的更高的地方跳,但这个判断往往只是个不准确的趋势
  • 它可能一次就跳到山顶,也可能跳过头翻到对面去;
  • 不过没关系,兔子翻过去之后还会跳回来;
  • 关键在于:随着时间推移,兔子逐渐冷静下来,每次跳得更加谨慎、步长更小,以收敛到合适的最优点。

"兔子逐渐变得清醒的过程"就是降温过程——温度参数 $t$ 在爬山过程中不断减小,从而控制每次更新的步长,防止在最优解附近来回震荡、永远不收敛。

降温参数怎么选

降温参数是略小于 $1$ 的常数,实践中一般在:

$$[0.985,\ 0.999]$$

区间内选取。参数越接近 $1$,降温越慢,迭代次数越多、搜索越充分,但耗时也越长;参数越远离 $1$,降温越快,迭代提前结束,结果可能粗糙。需要结合数据规模与时限折中选择。

通用伪代码框架

综合原文档与仓库参考代码,爬山算法的通用流程可以归纳为:

初始化当前解 x(通常取样本的重心/平均值,减少搜索量) 设定初始温度 t 与降温参数 rate while t > 阈值: 根据目标函数计算反馈(如梯度方向的累加量 cans) x = x + cans * t # 用温度控制步长 t = t * rate # 降温 输出 x

注意:更新时不能直接加上改变值,而要加上"改变值与温度的乘积"——这是算法能够收敛的关键。


三、例题 1:JSOI2008 球形空间产生器

原文档的第一道例题来自 JSOI2008 的经典问题(可在洛谷 P4035 找到原题)。

题目描述

给出 $n$ 维空间中的 $n+1$ 个点,已知它们在同一个 $n$ 维球面上,求出球心。 数据范围:$n \leq 10$,坐标绝对值不超过 $20000$。

为什么可以用爬山

球心到球面上每个点的距离都等于半径,因此"各点到球心距离的方差"是关于球心位置的函数,且很明显是单峰函数——偏离真实球心越远,距离越不均匀。这正好落在爬山算法适用范围内。

算法流程(5 步)

原文档给出了完整流程,这里逐一展开:

  1. 初始化球心为重心:将球心初始化为各给定点各维坐标的平均值(即重心),以先验地靠近真实球心,减少后续枚举量;
  2. 计算平均距离:对当前球心,求出每个已知点到该球心的欧氏距离的平均值$tot$;
  3. 遍历所有点计算改变值:记录一个改变值 $cans$(每一维度分别记录)。对每个点,将其欧氏距离与平均值比较——大于平均值则把差值加入改变值,否则减去。原文档特别指出:实际上并不用判断大小,只要不考虑绝对值、直接用坐标计算即可(参考代码中直接累加(dis[i] - tot) * (f[i][j] - ans[j]) / tot);
    • 形象理解:这个过程相当于把新球心"在空间里推来推去"——碰到太远的点就朝点方向拉一点,碰到太近的点就朝反方向推一点;
  4. 乘温度更新球心:将 $cans$ 乘上当前温度 $t$,更新球心各维坐标,回到步骤 2 继续迭代;
  5. 温度降到阈值以下时结束,输出最终球心。

关键点再次强调:更新球心时不能直接加改变值,而要加上改变值与温度的乘积

仓库参考代码逐段解析

仓库中的完整实现位于 hill-climbing_1.cpp,核心结构如下:

check()函数——计算每个维度的修正量:

void check() { tot = 0; for (int i = 1; i <= n + 1; i++) { dis[i] = 0; cans[i] = 0; for (int j = 1; j <= n; j++) dis[i] += (f[i][j] - ans[j]) * (f[i][j] - ans[j]); dis[i] = sqrt(dis[i]); // 欧氏距离 tot += dis[i]; } tot /= (n + 1); // 平均距离 for (int i = 1; i <= n + 1; i++) for (int j = 1; j <= n; j++) cans[j] += (dis[i] - tot) * (f[i][j] - ans[j]) / tot; // 欧氏距离差 * 差值贡献,按维度累加 }

main()函数——初始化与降温循环:

int main() { cin >> n; for (int i = 1; i <= n + 1; i++) for (int j = 1; j <= n; j++) { cin >> f[i][j]; ans[j] += f[i][j]; } for (int i = 1; i <= n; i++) ans[i] /= (n + 1); // 初始化为重心 for (double t = 10001; t >= 0.0001; t *= 0.99995) { // 不断降温 check(); for (int i = 1; i <= n; i++) ans[i] += cans[i] * t; // 按温度缩放步长 } cout << fixed << setprecision(3); for (int i = 1; i <= n; i++) cout << ans[i] << ' '; }

从源码可以看到几个典型的参数选择:

  • 初始温度$t = 10001$,终止阈值$t \ge 0.0001$;
  • 降温系数$0.99995$,落在文档建议的 $[0.985, 0.999]$ 区间附近且更接近 1——因为维度可达 10 且坐标可达 $20000$,需要更长迭代来精细收敛;
  • 输出保留 3 位小数setprecision(3))。

配套测试数据验证

仓库在 docs/misc/examples/hill-climbing/hill-climbing_1.in 提供了可复现测试:

2 0.0 0.0 -1.0 1.0 1.0 0.0

即 $n=2$,三个点 $(0,0)$、$(-1,1)$、$(1,0)$。对应的标准答案(hill-climbing_1.ans)为:

0.500 1.500

读者可以自行验证:点 $(0.5, 1.5)$ 到三个点的欧氏距离均为 $\sqrt{2.5}$,确实是这三点所在圆的圆心。程序在给定参数下能稳定收敛到该值。


四、例题 2:BZOJ 3680 吊打 XXX

第二道例题是 BZOJ 3680(可在 hydro.ac 的 BZOJ 题库找到原题)。

题目描述

求 $n$ 个点的带权类费马点

简单说,就是给定平面上 $n$ 个带权点 $(x_i, y_i, w_i)$,求一个点使得 $\sum_i w_i \cdot dist(P, P_i)$ 最小——即带权距离和最小点。由于引入了权重,这是一类"物理意义明确"的最优化问题。

解答思路:套用爬山框架 + 物理知识

原文档的解答非常简练:"框架类似,用了点物理知识。"具体而言:

  • 目标函数:$\sum_i w_i \cdot d_i$,其中 $d_i$ 是候选点到第 $i$ 个点的欧氏距离;
  • 物理直觉:把每个已知点想象成用弹簧/绳子拉着候选点,力的大小正比于权重,方向指向各自已知点;候选点的平衡位置就是"合力为零"的点,即类费马点;
  • 爬山迭代:每一轮把所有点对候选点的"拉力"(带权单位向量)累加得到合力方向,沿合力方向按当前温度步长移动候选点,不断降温直至收敛。

仓库参考代码解析

完整实现见 hill-climbing_2.cpp,核心的hillclimb()函数:

void hillclimb() { double t = 1000; while (t > 1e-8) { double nowx = 0, nowy = 0; for (int i = 1; i <= n; ++i) { double dx = x[i] - ansx, dy = y[i] - ansy; double dis = sqrt(dx * dx + dy * dy); nowx += (x[i] - ansx) * w[i] / dis; // 带权单位向量(拉力) nowy += (y[i] - ansy) * w[i] / dis; } ansx += nowx * t, ansy += nowy * t; // 按温度缩放步长移动 if (t > 0.5) t *= 0.5; // 前期快速降温 else t *= 0.97; // 后期精细降温 } }

这份代码展示了与原文档完全一致的框架,但降温策略更具技巧性——分段降温

  • 初始温度 $t = 1000$,终止条件 $t > 10^{-8}$;
  • 当 $t > 0.5$ 时,每轮乘以 $0.5$(快速逼近大致区域);
  • 当 $t \le 0.5$ 时,每轮乘以 $0.97$(慢速精细收敛)。

这种"先快后慢"的降温策略在实践中非常有效:前期大步长快速接近最优区域,后期小步长精细逼近,兼顾效率与精度。

配套测试数据验证

仓库提供了测试数据(hill-climbing_2.in):

3 0 0 1 0 2 1 1 1 1

即三个等权点 $(0,0)$、$(0,2)$、$(1,1)$ 求类费马点。标准答案(hill-climbing_2.ans)为:

0.577 1.000

其中 $0.577 \approx 1/\sqrt{3}$,恰好是等腰三角形费马点的经典位置,验证了算法结果的正确性。


五、优化:多次爬山与全局最优

单次爬山的质量高度依赖初始点位置。为了尽可能获取优秀答案,原文档给出的标准优化手段是多次爬山

  • 修改初始状态:随机或按不同策略生成多个不同的初始点;
  • 修改降温参数:使用不同的降温系数运行多轮;
  • 修改初始温度:改变搜索初期的步长规模;
  • 记录全局最优解:每轮爬山结束后,将本轮结果与历史最优比较,更新全局最优答案。

伪代码如下:

全局最优 = 初始解 重复若干次: 以不同初始状态/参数运行爬山得到局部最优 x 若 x 优于全局最优: 全局最优 = x 输出全局最优

原文档同时给出了一个重要的实战警示:多次爬山可能超时。因此在正式考试/比赛中,务必手造大数据测试并调整参数(轮数、初始温度、降温系数、终止阈值),在运行时间与答案精度之间取得平衡。


六、劣势:为何要走向模拟退火

爬山算法的劣势上文已经反复提及:它容易陷入局部最优解。当目标函数不是单峰函数时,这个劣势是致命的——贪心的"只接受更优解"策略使算法永远无法从局部峰"下山"去寻找更高的山峰。

正因如此,OI-wiki 将模拟退火(Simulated Annealing)作为爬山算法的直接后继者进行介绍:模拟退火在爬山的基础上引入了以一定概率接受劣解的机制,允许算法在温度较高时"跳下山谷",从而以较大概率逃离局部最优,逼近全局最优。读者可继续阅读 模拟退火 一文,了解两者在机制与适用场景上的差异。


七、总结与适用场景速查

维度说明
适用前提能判断两个解孰优孰劣,但难以写出严格正解;目标函数最好近似单峰
典型场景计算几何、数学题中的高维最优化(如求球心、费马点)
核心机制沿反馈方向按"温度 × 修正量"步长迭代,温度逐渐降低
关键参数初始温度、降温系数(建议 $[0.985, 0.999]$)、终止阈值、迭代次数
常用优化多次爬山 + 全局最优记录;分段降温(先快后慢)
主要劣势多峰函数下易陷入局部最优;可通过模拟退火弥补

配套资源索引:

  • 例题 1 完整代码:hill-climbing_1.cpp,测试数据:hill-climbing_1.in 与 hill-climbing_1.ans;
  • 例题 2 完整代码:hill-climbing_2.cpp,测试数据:hill-climbing_2.in 与 hill-climbing_2.ans;
  • 算法插图:docs/misc/images/hill-climbing.png;
  • 相关算法文档:模拟退火。

【免费下载链接】OI-wiki:star2: Wiki of OI / ICPC for everyone. (某大型游戏线上攻略,内含炫酷算术魔法)项目地址: https://gitcode.com/GitHub_Trending/oi/OI-wiki

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 17:18:27

Boost.ASIO实现STOMP客户端:帧编解码、异步收发与心跳机制

简介&#xff1a;面向C网络开发者的STOMP客户端源码包&#xff0c;基于Boost.ASIO异步I/O库实现&#xff0c;清晰演示如何与RabbitMQ、ActiveMQ等消息代理建立连接并完成订阅、发送与接收消息&#xff0c;适合正在学习C异步网络编程或希望接入消息中间件的开发者参考。STOMP是轻…

作者头像 李华
网站建设 2026/9/13 17:18:25

图莫斯TOOMOSS_OpenDev(CAN) VI深度解析:UDS诊断句柄与设备抽象层设计

1. 这不是普通LabVIEW CAN控件——图莫斯TOOMOSS_OpenDev(CAN).vi的本质定位与设计逻辑 你打开LabVIEW&#xff0c;拖一个CAN VISA节点&#xff0c;配置波特率、通道号&#xff0c;点运行——结果报错“CAN device not found”或者“Access denied”。再换一个第三方驱动&#…

作者头像 李华
网站建设 2026/9/13 17:16:10

零基础转行机器人工程师?6个月学习路线全拆解

这两年我被人问得最多的一个问题就是&#xff1a;零基础&#xff0c;6个月能不能转行做机器人&#xff1f;问的人里有学机械的、学计算机的&#xff0c;有干电气维修想转的&#xff0c;还有写前端想跨界过来的。我的答案一直很直接&#xff1a;能&#xff0c;但有前提。前提是你…

作者头像 李华
网站建设 2026/9/13 17:14:33

西安嵌入式培训实录:从点灯到芯片原语层的能力跃迁

1. 项目概述&#xff1a;一场西安嵌入式培训实地探查带来的认知刷新 “深挖西安嵌入式培训班&#xff01;看完直接打破我的固有认知”——这个标题不是营销噱头&#xff0c;而是我作为在嵌入式行业摸爬滚打十二年、带过三届校企联合实训班、亲手调试过从ARM7到RISC-V全系开发板…

作者头像 李华