news 2026/9/20 13:47:40

Swift 算法俱乐部:模拟退火算法(Simulated Annealing)原理、Swift 实现与 TSP 实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Swift 算法俱乐部:模拟退火算法(Simulated Annealing)原理、Swift 实现与 TSP 实战
  • 示例工程
  • 教程

【免费下载链接】swift-algorithm-club

Algorithms and data structures in Swift, with explanations!

项目地址:https://gitcode.com/gh_mirrors/sw/swift-algorithm-club
点击查看免费下载

模拟退火(Simulated Annealing)是一种受金属退火工艺启发的全局优化元启发式算法,用于在(通常是离散的)大型搜索空间中逼近全局最优解。本指南以 Swift Algorithm Club 仓库中Simulated annealing/目录下的实现为核心,系统讲解其原理、伪代码、通用 Swift 框架,并结合 20 城市旅行商问题(TSP)的完整示例,帮助你掌握该算法的参数含义、接受准则与调优思路。读完本文,你将能够独立使用仓库中的SimulatedAnnealing泛型框架求解自己的组合优化问题。

算法思想:从冶金退火到组合优化

模拟退火的名字源自冶金学中的"退火"工艺:材料被加热到高温后,在受控条件下缓慢冷却,从而消除内部缺陷、提升强度与耐久性。这一过程在数学上被类比为在一个巨大的搜索空间中寻找最小代价解——算法通过利用热力学系统的特性(温度、能量、随机扰动)来逼近全局最优。

从计算角度看,它属于元启发式算法(metaheuristic),核心目标是在往往包含大量局部最优解的搜索空间中,以可接受的计算代价近似求得全局最优解。仓库根目录的 README.markdown 将其概括为"用于在(通常是离散的)大型搜索空间中逼近全局最大值的概率技术"。

与爬山法(Hill Climbing)的本质差异

爬山法是一类经典的局部搜索技术,它不允许向下的移动(即只接受使目标函数变差的解不合法),因此极易陷入局部最优解而无法自拔。模拟退火的关键突破在于:允许一定概率接受"更差"的解(向下移动),从而具备逃出局部最优的能力。

这种允许向下移动的概率具有鲜明的温度依赖特性:

  • 高温阶段:接受"差解"的概率很高。此时接受函数表现出类似混沌的行为,搜索空间被大幅放宽,有助于大范围探索、跳出局部最优;
  • 低温阶段:接受"差解"的概率逐渐降低。搜索空间被收窄,算法把注意力集中在局部改进上,趋于稳定收敛。

简言之,温度相当于一个"调节旋钮":高温负责全局探索(exploration),低温负责局部开发(exploitation),二者在同一套接受准则下平滑过渡。

算法流程与伪代码

原文档给出了完整的伪代码骨架,其输入为四个要素:初始解、初始温度、冷却速率和接受函数;输出为搜索到的最优解Sbest

Input: initial, temperature, coolingRate, acceptance Output: Sbest Scurrent <- CreateInitialSolution(initial) Sbest <- Scurrent while temperature is not minimum: Snew <- FindNewSolution(Scurrent) if acceptance(Energy(Scurrent), Energy(Snew), temperature) > Rand(): Scurrent = Snew if Energy(Scurrent) < Energy(Sbest): Sbest = Scurrent temperature = temperature * (1-coolingRate)

逐步拆解这段伪代码,可以看到算法由四个核心环节构成:

  1. 初始化:基于输入构造一个初始可行解Scurrent,并暂记为当前最优Sbest
  2. 邻域扰动:在当前解附近随机生成新解Snew(在 TSP 示例中体现为交换两个城市的位置);
  3. 概率接受:调用接受函数,将其返回值与随机数Rand()比较,若满足条件则接受新解为当前解——这是模拟退火区别于爬山法的关键一步;
  4. 降温调度:每轮迭代末尾按temperature = temperature * (1 - coolingRate)降低温度,直到温度降至最小值(仓库实现中的终止条件为temp > 1)为止。

值得注意,伪代码中if Energy(Scurrent) < Energy(Sbest)这一步保证了Sbest只记录历史最优,因此即便算法在高温阶段频繁接受差解导致当前解变差,最优解也不会丢失——这是模拟退火工程实现中的标准防御手段。

常见接受准则:Metropolis 判据

模拟退火的接受准则有多种形式,原文档给出的是最经典的基于指数分布的判据:

P(accept) <- exp((e - ne) / T)

其中:

符号含义
e当前解的能量(当前解对应的代价,如 TSP 中的总路程)
ne新解的能量(新解对应的代价)
T当前温度

该公式的物理直觉非常清晰:

  • 当新解更优(ne < e)时,(e - ne) > 0,指数大于 1,接受概率被截断为 1,即无条件接受改进
  • 当新解更差(ne > e)时,指数为负,接受概率落在(0, 1)区间内,且温度越高、代价差距越小,接受概率越大——这正是高温阶段容忍"坏移动"、低温阶段拒绝"坏移动"的数学来源。

仓库示例代码中的接受函数实现(见 simann_example.swift)与上述公式完全一致:

let acceptance : AcceptanceFunc = { (e: Double, ne: Double, te: Double) -> Double in if ne < e { return 1.0 } return exp((e - ne) / te) }

注意其与伪代码的对应关系:e对应当前能量ene对应新能量nete对应当前温度T

仓库源码剖析:通用泛型框架 simann.swift

仓库将模拟退火的核心逻辑抽象为一个与具体问题解耦的泛型函数,定义在 simann.swift 中。理解这套抽象,你就能把算法复用到任何"可以衡量能量、可以随机扰动"的问题上。

三个关键抽象

protocol Clonable { init(current: Self) } protocol SAObject: Clonable { var count: Int { get } func randSwap(a: Int, b: Int) func currentEnergy() -> Double func shuffle() } typealias AcceptanceFunc = (Double, Double, Double) -> Double
  • Clonable:通过init(current:)从现有实例克隆出新实例。由于模拟退火每轮都要在"当前解副本"上做扰动、并在接受时替换当前解,深拷贝是保证迭代正确性的前提。扩展中还为Array提供了clone()支持(元素同样须为Clonable),用于复制整个解序列;
  • SAObject:定义了算法对"解对象"的全部要求——元素个数count、随机交换两个位置的randSwap(a:b:)、计算当前能量的currentEnergy()、随机洗牌的shuffle()。任何满足该协议的类型都可以作为算法的输入;
  • AcceptanceFunc:接受函数的类型别名,签名为(当前能量, 新能量, 温度) -> Double,返回一个概率值,供主循环与随机数比较。

主循环实现

func SimulatedAnnealing<T: SAObject>(initial: T, temperature: Double, coolingRate: Double, acceptance: AcceptanceFunc) -> T { var temp: Double = temperature var currentSolution = initial.clone() currentSolution.shuffle() var bestSolution = currentSolution.clone() while temp > 1 { let newSolution: T = currentSolution.clone() let pos1: Int = Int.random(in: 0 ..< newSolution.count) let pos2: Int = Int.random(in: 0 ..< newSolution.count) newSolution.randSwap(a: pos1, b: pos2) let currentEnergy: Double = currentSolution.currentEnergy() let newEnergy: Double = newSolution.currentEnergy() if acceptance(currentEnergy, newEnergy, temp) > Double.random(in: 0 ..< 1) { currentSolution = newSolution.clone() } if currentSolution.currentEnergy() < bestSolution.currentEnergy() { bestSolution = currentSolution.clone() } temp *= 1-coolingRate } return bestSolution }

对照伪代码,逐条对应实现细节:

伪代码步骤Swift 实现位置说明
初始化解initial.clone()+shuffle()先克隆输入,再随机洗牌生成随机可行解
记录最优bestSolution = currentSolution.clone()以随机初始解作为首个候选最优
邻域扰动randSwap(a:pos1, b:pos2)随机选取两个下标并交换,pos1/pos2均为0..<count内均匀随机
概率接受acceptance(...) > Double.random(in: 0..<1)接受函数输出与[0,1)均匀随机数比较,与伪代码> Rand()一一对应
更新最优currentSolution.currentEnergy() < bestSolution.currentEnergy()能量越低越好,仅记录改进
降温temp *= 1-coolingRate与伪代码temperature * (1-coolingRate)完全一致

两个值得注意的实现细节:

  • 终止条件:仓库实现以while temp > 1作为停止条件,即"温度降至 1 以下视为冷却完毕"。这是一个隐式的"最小温度"设定,你也可以根据问题规模调整该阈值;
  • 能量缓存:示例中的Tour.currentEnergy()使用了energy属性缓存,首次计算后复用结果,避免重复遍历整条回路(详见下文)。

实战案例:20 城市旅行商问题(TSP)

原文档指出,仓库用该算法求解了一个包含20 个城市的旅行商问题实例,完整代码位于 simann_example.swift。TSP 的目标是找到一条访问所有城市恰好一次并返回起点的最短回路——它是一个经典的 NP 难组合优化问题,也是模拟退火最经典的应用场景之一。

数据结构:Point 与 Tour

class Point: Clonable { var x: Int var y: Int init(x: Int, y: Int) { self.x = x; self.y = y } required init(current: Point){ self.x = current.x self.y = current.y } }

Point用整数坐标(x, y)表示一个城市,并实现Clonable协议。两点间的欧几里得距离通过自定义中缀运算符<->计算(采用平方和开方,等价于两点间的直线距离):

infix operator <->: AdditionPrecedence extension Point { static func <-> (left: Point, right: Point) -> Double { let xDistance = (left.x - right.x) let yDistance = (left.y - right.y) return Double((xDistance * xDistance) + (yDistance * yDistance)).squareRoot() } }

Tour则是SAObject的具体实现,内部以Points(即[Point])保存城市访问顺序:

class Tour: SAObject { var tour: Points var energy: Double = 0.0 var count: Int { return self.tour.count } ... }

其三个协议方法的实现如下:

func randSwap(a: Int, b: Int) -> Void { let (cpos1, cpos2) = (self[a], self[b]) self[a] = cpos2 self[b] = cpos1 } func currentEnergy() -> Double { if self.energy == 0 { var tourEnergy: Double = 0.0 for i in 0..<self.count { let fromCity = self[i] var destCity = self[0] if i+1 < self.count { destCity = self[i+1] } let e = fromCity<->destCity tourEnergy = tourEnergy + e } self.energy = tourEnergy } return self.energy } func shuffle() { self.tour.shuffle() }
  • randSwap交换两个下标位置的城市,即"2-opt"式的最小邻域扰动;
  • currentEnergy从第 0 个城市出发,依次累加相邻城市间距,最后一个城市与首城相连(destCity = self[0]兜底),得到整条回路的总路程;该值即"能量",越小越好;
  • shuffle直接复用 Swift 标准库的Array.shuffle()打乱访问顺序,用于生成随机初始解。

20 个城市的输入数据

示例中的城市坐标如下(共 20 个点,分布在 20×200 的平面区域内):

let points: [Point] = [ (60 , 200), (180, 200), (80 , 180), (140, 180), (20 , 160), (100, 160), (200, 160), (140, 140), (40 , 120), (100, 120), (180, 100), (60 , 80) , (120, 80) , (180, 60) , (20 , 40) , (100, 40) , (200, 40) , (20 , 20) , (60 , 20) , (160, 20) , ].map{ Point(x: $0.0, y: $0.1) }

注意其中出现了三处重复坐标(60, 200)(60, 20)不同,但(60, 200)与列表末段的(60, 20)之外,(100, 160)(100, 120)等均不重复;真正重复的是(20, 160)(20, 40)?——实际上按坐标逐一核对,所有 20 个点坐标均互不相同)。从源码结构看,这里直接以元组字面量配合map构造点集,坐标布局接近均匀网格,便于直观验证最终回路形态。

参数设定与运行

示例以如下参数调用算法:

let result: Tour = SimulatedAnnealing(initial : Tour(points: points), temperature : 100000.0, coolingRate : 0.003, acceptance : acceptance)
  • 初始温度100000.0:足够高,保证初期几乎接受任意扰动,充分探索解空间;
  • 冷却速率0.003:每轮降温0.3%。以temp > 1为终止条件推算,从 100000 降到 1 需要约ln(100000)/0.003 ≈ 3837轮迭代;
  • 接受函数:即上文展示的指数判据。

运行后,SimulatedAnnealing会在入口与出口分别打印初始解与最优解的能量:

Initial solution: <随机初始回路的总路程> Best solution: <退火搜索后的最优总路程>

由于初始解来自随机洗牌,两次运行的结果会有所不同,但最终能量通常显著低于初始随机解——这正是模拟退火在有限迭代内逼近较优解的证据。该示例文件顶部包含平台适配代码(os(OSX)引入Foundation/Cocoaos(Linux)引入Glibc),因此既可在 macOS 也可在 Linux 上使用 Swift 直接运行:swift simann_example.swift

参数调优:四个输入如何影响收敛

原文档强调算法有四个输入参数,理解它们对工程实践至关重要:

参数作用仓库示例取值调优方向
initial(初始解)退火起点,示例中为随机洗牌后的 20 城市回路Tour(points: points)可用贪心解等更优起点加速收敛
temperature(初始温度)控制初期接受差解的概率,决定探索强度100000.0过小易早熟陷入局部最优;过大则浪费大量迭代在纯随机徘徊上
coolingRate(冷却速率)每轮降温比例,决定降温快慢与总迭代数0.003过大会降温过快、来不及收敛;过小则迭代次数剧增
acceptance(接受函数)决定"差解"的接受概率,(e, ne, T) -> 概率Metropolis 指数判据可替换为其他准则以适配不同问题

从 simann.swift 的实现可以看到三者如何协同:初始温度与冷却速率共同决定总迭代轮数(约ln(最小温度/初始温度) / coolingRate),接受函数则决定每一轮是否接受扰动。若冷却速率过慢而迭代上限不足,算法可能尚未冷却即被截断;若初始温度过低,接受函数在初期就趋于拒绝差解,退化回爬山法。

适用场景与注意事项

模拟退火的优势使其特别适合以下场景:

  • 解空间巨大且离散的组合优化问题,如 TSP、排程、布局、图划分等;
  • 目标函数可计算、邻域结构可定义,但无法保证多项式时间内求出精确最优解的问题;
  • 对解质量要求较高、但可接受"近似最优"的应用(原文档将其明确定位为"逼近全局最优"的元启发式)。

使用时的注意事项同样值得强调:

  • 结果具有随机性:初始解洗牌与每轮的随机数均引入不确定性,正式使用时应多次运行取最优,或引入固定随机种子便于复现;
  • 参数高度依赖问题规模:仓库示例中100000的初始温度与0.003的冷却速率是针对 20 城市实例调出的,换用更大规模问题需重新标定;
  • 能量函数需单调可比:算法依赖"能量越低越好"这一约定(currentEnergy() < bestSolution.currentEnergy()),若你的目标是最大化,需将目标函数取负或取倒数。

延伸阅读与相关资源

  • 算法的通用框架与协议抽象见 simann.swift,完整 TSP 示例见 simann_example.swift,原始说明文档见 Simulated annealing/README.md;
  • 仓库中还收录了另一类"模拟生物进化"的元启发式算法——遗传算法(Genetic),其文档同样以旅行商问题为示例,可与模拟退火对照学习;
  • TSP 属于指数级复杂度的代表问题,关于其计算复杂度的讨论可参考 Big-O Notation.markdown 中对O(2^n)的介绍。

本文介绍的实现由 Mike Taghavi(mitghi)为 Swift Algorithm Club 编写,采用 MIT 许可协议(见 simann.swift 文件头声明),你可以在遵守协议的前提下将其复用到自己的项目中。

  • 示例工程
  • 教程

【免费下载链接】swift-algorithm-club

Algorithms and data structures in Swift, with explanations!

项目地址:https://gitcode.com/gh_mirrors/sw/swift-algorithm-club
点击查看免费下载

相关推荐

上一篇:3分钟掌握专业电路绘图:Draw.io电子工程库完整指南
下一篇:Qwen Code IDE 集成实战:通过 MCP 协议把终端 Agent 接入 VS Code 生态

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

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

Halcon + C#:工业读码与OCR识别的落地方案

简介&#xff1a;这份资源是一套基于 C# 与 Halcon 的二维码深度识别与 OCR 示例工程&#xff0c;面向需要在 Windows 桌面应用中集成机器视觉能力的开发者和自动化项目人员。项目以 WindowsFormsApp1 为入口&#xff0c;完整演示了图像捕获、预处理、二维码定位与解码、文字识…

作者头像 李华
网站建设 2026/9/20 13:40:56

编码智能体执行框架(Harness)设计实证研究

编码智能体执行框架&#xff08;Harness&#xff09;设计实证研究 arXiv编号&#xff1a;arXiv:2609.20804v1 [cs.AI] 摘要 编码智能体执行框架&#xff08;coding harness&#xff09;决定大模型如何把模型原生能力转化为长视界软件工程任务性能。现有工作大多将执行框架作为完…

作者头像 李华
网站建设 2026/9/20 13:39:22

VLSI设计基础复习全攻略:核心考点、经典题型与备考避坑指南

简介&#xff1a;这份VLSI设计基础复习资料面向微电子、集成电路及相关专业学生&#xff0c;适用于期末备考、考研复试和入职前知识梳理。内容以问答形式系统梳理CMOS工艺成为主流的原因、MOS器件与BJT的工作原理区别、硅栅工艺的优势、阈值电压影响因素、萨式方程、衬底偏置效…

作者头像 李华