1. 项目概述:当优化难题遇上“退火”智慧
如果你正被一个需要从成百上千个可能性中找出“最优解”的问题所困扰,比如规划一条走遍所有城市且总路程最短的旅行路线,或者为工厂里的机器安排最高效的生产顺序,那么“模拟退火算法”很可能就是你工具箱里缺失的那把瑞士军刀。这个算法的名字听起来有点玄乎,但它背后的思想却异常直观和优美——灵感直接来源于冶金学中的“退火”工艺。简单来说,金属工匠通过先将材料加热到高温,再极其缓慢地冷却,从而消除内部应力,让金属原子找到能量最低、结构最稳定的排列方式。模拟退火算法正是借鉴了这一过程,将其抽象为一种解决复杂组合优化问题的通用策略。
我最初接触这个算法是为了解决一个经典的“旅行商问题”(TSP):给定一系列城市和每对城市之间的距离,找到一条访问每个城市恰好一次并回到起点的最短可能回路。这问题看似简单,但随着城市数量增加,可能的路线数量会呈爆炸式增长(n个城市有(n-1)!/2条可能路线),用穷举法在现实时间尺度内根本不可行。模拟退火提供了一种跳出局部最优解、向全局最优解“渐进”的聪明办法。它允许我们在搜索过程中偶尔接受一些“更差”的解决方案,就像高温下的金属原子有更高动能可以跳出当前位置一样,从而有机会逃离那些看似不错但并非最佳的“陷阱”。
本文将手把手带你用Python实现一个完整的模拟退火算法来解决TSP问题。我会从最核心的物理隐喻讲起,拆解算法的每一个参数和步骤,然后给出可以直接运行、逐行注释的源码。更重要的是,我会分享在实际调参和编码中踩过的坑和总结出的经验,比如如何设计“邻域”动作、如何设置降温计划才能既快又好,以及如何可视化地观察算法的搜索过程。无论你是算法初学者,还是需要快速解决一个实际优化问题的工程师,这篇文章都能给你一个清晰、可操作的路线图。
2. 模拟退火算法核心原理与TSP问题建模
2.1 从冶金退火到算法隐喻
要理解模拟退火,我们必须先搞懂它的物理原型。在冶金学中,退火是一种热处理工艺,用于增加材料的延展性和韧性。过程分为三步:加热、保温和缓慢冷却。加热使原子获得足够的动能,脱离原来的位置随机移动;保温让原子有充分时间进行重排;而缓慢冷却则使得原子有足够的时间,随着温度降低,逐渐移动到能量更低、更稳定的晶格位置上。如果冷却过快(淬火),原子就会被“冻结”在非稳定的高能状态,导致材料硬而脆。
算法将这一过程完美映射:
- 系统状态:对应一个候选解(在TSP中就是一条旅行路线)。
- 能量:对应目标函数值(在TSP中就是路线的总距离)。我们的目标是找到能量最低(距离最短)的状态。
- 温度:一个控制算法行为的核心参数。高温时,算法有高概率接受比当前解更差的解(即“坏移动”),搜索范围广,倾向于探索;低温时,算法几乎只接受更好的解,搜索范围集中,倾向于在局部进行精细优化。
- 退火计划:即温度如何随时间(或迭代次数)下降的策略。这是算法性能的关键。
算法的精髓在于其Metropolis接受准则:对于一个新产生的候选解,如果其能量低于当前解(更优),则总是接受;如果能量更高(更差),则以一个概率P接受,这个概率P取决于能量差ΔE和当前温度T:P = exp(-ΔE / T)。温度T很高时,即使ΔE很大,exp(-ΔE / T)也可能接近1,意味着很大概率接受差解,促进了全局探索。随着T逐渐降低,接受差解的概率越来越小,算法最终稳定在一个(希望是全局的)最优解附近。
2.2 TSP问题的数学描述与挑战
旅行商问题可以形式化地描述:给定一个完全图G=(V, E),其中V是城市集合,|V|=n,E是连接所有城市的边集合,每条边(i, j)有一个非负权重d_{ij}表示距离。目标是找到一个哈密顿回路(经过每个顶点恰好一次的环),使得回路的总权重最小。
其挑战在于解空间的规模。n个城市的对称TSP,可能的哈密顿回路有(n-1)!/2条。当n=20时,解的数量约为6.08×10^16,这是一个天文数字。因此,对于稍大规模的问题(n>30),精确算法(如动态规划、分支定界)将变得不可行,我们必须依赖像模拟退火、遗传算法这样的启发式或元启发式算法来寻找高质量近似解。
在代码中,一个解通常表示为一个城市的排列(列表)。例如,对于5个城市[0,1,2,3,4],一个可能的解是[0, 2, 1, 4, 3],表示旅行商从城市0出发,依次访问城市2、1、4、3,最后返回城市0。我们需要计算这个排列所对应回路的总距离。
2.3 算法流程的伪代码与关键组件
在深入代码之前,让我们用伪代码梳理整个流程,这有助于理解各个模块如何衔接:
1. 初始化: - 生成一个初始解 S(例如随机排列) - 设置初始温度 T_start,终止温度 T_end,降温系数 alpha - 设置每个温度下的迭代次数 L(马尔可夫链长度) - 令当前解 S_current = S,当前能量 E_current = cost(S) - 令最优解 S_best = S_current,最优能量 E_best = E_current 2. 外循环:当温度 T > T_end 时,重复: a. 内循环:重复 L 次: i. 通过“邻域动作”从 S_current 产生一个新解 S_new ii. 计算能量差 ΔE = cost(S_new) - cost(S_current) iii. 如果 ΔE < 0 (新解更优): 接受 S_new: S_current = S_new, E_current = cost(S_new) 如果 E_current < E_best: 更新最优解: S_best = S_current, E_best = E_current iv. 否则(新解更差): 以概率 P = exp(-ΔE / T) 接受 S_new 如果接受,则 S_current = S_new, E_current = cost(S_new) b. 降温: T = T * alpha (或其他降温策略) 3. 输出最优解 S_best 及其对应的能量 E_best从这个流程可以看出,除了核心的Metropolis准则,还有几个需要我们精心设计的部分:
- 初始解生成:可以是完全随机,也可以使用一个简单启发式算法(如最近邻法)得到一个较好的起点,可能加速收敛。
- 邻域动作:如何从当前解产生一个“邻近”的新解。这对搜索效率至关重要。
- 退火计划:包括初始温度、终止温度、降温系数和链长L。这需要根据问题调整。
- 能量(成本)函数:即TSP中的总路径距离,需要高效计算。
注意:模拟退火不能保证找到全局最优解,但它能以很高的概率找到接近全局最优的解,并且在计算时间和解的质量之间提供了一个很好的权衡。它的魅力在于其通用性——只要你能定义解的表达、邻域动作和成本函数,它就能被应用于几乎任何离散优化问题。
3. Python实现详解:从环境准备到核心代码
3.1 环境配置与数据准备
我们不需要复杂的库。核心实现仅需numpy进行高效的数学计算和数组操作,以及matplotlib用于结果可视化。如果你使用Anaconda,这些库通常已经安装。否则,可以通过pip安装:
pip install numpy matplotlib对于TSP问题,我们首先需要城市坐标数据。为了演示,我们可以随机生成一组二维平面上的城市坐标,也可以使用标准的TSP测试数据集(如TSPLIB中的berlin52)。这里我们先采用随机生成的方式,这样更容易理解和复现。
import numpy as np import matplotlib.pyplot as plt import random import math # 设置随机种子以保证结果可复现 np.random.seed(42) random.seed(42) # 生成模拟城市坐标:假设有50个城市,坐标范围在[0, 100]之间 num_cities = 50 cities = np.random.rand(num_cities, 2) * 100 # 计算城市间距离矩阵(欧几里得距离) # 这是一个对称矩阵,对角线为0 def compute_distance_matrix(cities): n = len(cities) dist_matrix = np.zeros((n, n)) for i in range(n): for j in range(i+1, n): # 利用对称性,只计算上三角 dist = np.linalg.norm(cities[i] - cities[j]) dist_matrix[i, j] = dist dist_matrix[j, i] = dist return dist_matrix distance_matrix = compute_distance_matrix(cities) print(f"生成了 {num_cities} 个城市。距离矩阵形状:{distance_matrix.shape}")计算距离矩阵是预处理中开销较大的部分,但只需计算一次。在算法迭代中,我们通过索引这个矩阵来快速计算路径总长,避免重复计算两点间距离。
3.2 核心算法类的构建
我们将算法封装成一个类SimulatedAnnealingTSP,这样结构更清晰,也便于管理状态和参数。
class SimulatedAnnealingTSP: def __init__(self, distance_matrix, T_start=1000, T_end=1e-3, alpha=0.99, L=100): """ 初始化模拟退火TSP求解器。 参数: distance_matrix: 城市距离矩阵,numpy二维数组。 T_start: 初始温度。 T_end: 终止温度。 alpha: 降温系数,每次温度乘以alpha。 L: 马尔可夫链长度,每个温度下的迭代次数。 """ self.dist_mat = distance_matrix self.num_cities = distance_matrix.shape[0] self.T_start = T_start self.T_end = T_end self.alpha = alpha self.L = L # 算法运行记录 self.best_solution = None self.best_cost = float('inf') self.current_solution = None self.current_cost = float('inf') self.cost_history = [] # 记录每次外循环后的当前成本 self.best_cost_history = [] # 记录每次外循环后的历史最优成本 self.temperature_history = [] # 记录温度变化 def total_distance(self, solution): """计算给定路径顺序的总距离。""" total = 0.0 for i in range(self.num_cities): total += self.dist_mat[solution[i], solution[(i+1) % self.num_cities]] return total def generate_initial_solution(self): """生成初始解:随机排列。""" solution = list(range(self.num_cities)) random.shuffle(solution) return solution def generate_neighbor(self, solution): """ 通过邻域动作产生一个新解。 这里采用经典的'2-opt'移动的一种简单形式:随机交换两个城市的位置。 这是一种简单但有效的扰动方式。 """ new_solution = solution.copy() # 随机选择两个不同的索引 i, j = random.sample(range(self.num_cities), 2) # 交换这两个位置的城市 new_solution[i], new_solution[j] = new_solution[j], new_solution[i] return new_solution def solve(self): """执行模拟退火算法主流程。""" # 初始化 self.current_solution = self.generate_initial_solution() self.current_cost = self.total_distance(self.current_solution) self.best_solution = self.current_solution.copy() self.best_cost = self.current_cost T = self.T_start iteration = 0 print("开始模拟退火优化...") while T > self.T_end: for _ in range(self.L): # 产生邻域解 new_solution = self.generate_neighbor(self.current_solution) new_cost = self.total_distance(new_solution) # 计算能量差 delta_cost = new_cost - self.current_cost # Metropolis接受准则 if delta_cost < 0: # 新解更好,总是接受 accept = True else: # 新解更差,以一定概率接受 accept_prob = math.exp(-delta_cost / T) accept = random.random() < accept_prob if accept: self.current_solution = new_solution self.current_cost = new_cost # 更新历史最优 if new_cost < self.best_cost: self.best_solution = new_solution.copy() self.best_cost = new_cost print(f"迭代 {iteration}, 温度 {T:.4f}, 发现更优解: {self.best_cost:.2f}") # 记录当前状态 self.cost_history.append(self.current_cost) self.best_cost_history.append(self.best_cost) self.temperature_history.append(T) # 降温 T *= self.alpha iteration += 1 # 可选:每100次外循环打印一次进度 if iteration % 100 == 0: print(f"进度: 迭代{iteration}, 温度{T:.4f}, 当前成本{self.current_cost:.2f}, 最优成本{self.best_cost:.2f}") print("优化完成!") print(f"最终最优路径成本: {self.best_cost:.2f}") return self.best_solution, self.best_cost这个类包含了算法的所有核心要素。generate_neighbor方法这里使用了最简单的“交换”操作,后续我们会探讨更高效的邻域动作。solve方法严格遵循了之前描述的伪代码流程。
3.3 可视化与结果分析模块
算法跑完了,我们还需要看看结果怎么样,以及算法是如何搜索的。可视化能给我们直观的反馈。
def plot_results(self, cities): """绘制优化结果:包括最终路径和收敛曲线。""" fig, axes = plt.subplots(1, 2, figsize=(14, 5)) # 子图1:最优路径图 ax1 = axes[0] # 绘制城市点 ax1.scatter(cities[:, 0], cities[:, 1], c='red', s=50, zorder=5) for i, (x, y) in enumerate(cities): ax1.text(x, y, str(i), fontsize=8, ha='center', va='center') # 绘制路径 best_route = self.best_solution + [self.best_solution[0]] # 使路径闭合 route_coords = cities[best_route, :] ax1.plot(route_coords[:, 0], route_coords[:, 1], 'b-', linewidth=1, alpha=0.7) ax1.set_xlabel('X坐标') ax1.set_ylabel('Y坐标') ax1.set_title(f'模拟退火求解TSP最优路径 (总距离: {self.best_cost:.2f})') ax1.grid(True, alpha=0.3) # 子图2:收敛曲线 ax2 = axes[1] iterations = range(len(self.cost_history)) ax2.plot(iterations, self.cost_history, 'g-', linewidth=0.5, alpha=0.7, label='当前成本') ax2.plot(iterations, self.best_cost_history, 'r-', linewidth=1.5, label='历史最优成本') ax2.set_xlabel('外循环迭代次数') ax2.set_ylabel('路径总距离') ax2.set_title('成本随迭代变化曲线') ax2.legend() ax2.grid(True, alpha=0.3) # 可选:添加第二个Y轴显示温度 ax2_temp = ax2.twinx() ax2_temp.plot(iterations, self.temperature_history, 'b--', linewidth=0.8, alpha=0.5, label='温度 (右轴)') ax2_temp.set_ylabel('温度 T') ax2_temp.legend(loc='upper right') plt.tight_layout() plt.show() def print_route(self): """打印最优路径顺序。""" print("最优访问顺序: ", ' -> '.join(map(str, self.best_solution)))现在,我们可以运行完整的流程:
# 实例化并运行算法 solver = SimulatedAnnealingTSP(distance_matrix, T_start=1000, T_end=1e-5, alpha=0.995, L=200) best_route, best_cost = solver.solve() # 可视化结果 solver.plot_results(cities) solver.print_route()运行这段代码,你会看到算法开始迭代,并在控制台打印发现更优解的信息。最终会弹出两个图表:左边是最优路径的示意图,右边是算法搜索过程中当前成本和历史最优成本的变化曲线,以及温度下降曲线。从收敛曲线你可以清晰地看到模拟退火的特征:初期成本波动剧烈(高温探索),后期波动平缓并稳定下降(低温利用)。
4. 算法调优与高级技巧:从能用变到好用
基础的实现已经能工作了,但可能距离“高效”和“鲁棒”还有差距。模拟退火算法的性能极大地依赖于参数设置和邻域动作的设计。这部分分享的调优经验,是文档里不会写的“实战心得”。
4.1 退火计划:温度与迭代的艺术
退火计划是算法的“调度表”,决定了搜索的广度和深度。没有放之四海而皆准的参数,但有一些经验法则和自适应策略。
1. 初始温度 T_start:初始温度应该足够高,使得算法在初期有足够高的概率接受差解。一个实用的方法是进行一段时间的“预热”采样:随机生成大量状态转移,计算能量差ΔE的平均值,然后根据一个较高的初始接受概率(例如0.8)来反推T_start:T_start = -ΔE_avg / ln(P_initial)。我们在代码中可以添加一个预热函数:
def estimate_initial_temperature(self, num_samples=1000, initial_accept_prob=0.8): """通过采样估计合适的初始温度。""" deltas = [] current = self.generate_initial_solution() current_cost = self.total_distance(current) for _ in range(num_samples): new = self.generate_neighbor(current) new_cost = self.total_distance(new) delta = new_cost - current_cost if delta > 0: # 只关注变差的情况 deltas.append(delta) # 更新当前解,继续采样 current, current_cost = new, new_cost if deltas: avg_delta = np.mean(deltas) T_start = -avg_delta / math.log(initial_accept_prob) return max(T_start, 1.0) # 保证至少为1 else: return 100.0 # 默认值2. 终止温度 T_end:通常设置得非常低(如1e-3, 1e-5),以确保算法有足够长的“低温淬火”阶段来进行局部精细优化。也可以设置当连续若干次迭代最优解不再改善时提前终止。
3. 降温系数 alpha:取值通常在0.9到0.999之间。alpha越接近1,降温越慢,搜索越充分,但耗时也越长。对于复杂问题,慢降温(高alpha)效果更好。一种改进是自适应降温:如果当前温度下接受率很高,说明温度可能偏高,可以适当加快降温;如果接受率很低,说明降温可能太快,应放缓或甚至短暂“回温”。
4. 马尔可夫链长度 L:每个温度下应进行足够次数的尝试,以达到“准平衡”状态。简单的做法是L设为城市数量的若干倍(如100*n)。更高级的做法是动态链长:当连续接受或拒绝一定数量的新解后,就提前结束当前温度的迭代。
4.2 邻域动作设计:效率提升的关键
之前我们用了简单的“交换”操作,但这在TSP中效率不是最高的。因为交换两个不相邻的城市可能会产生非常差的解,被接受的概率低。更常用的高效邻域动作是2-opt和3-opt。
2-opt操作:随机选择路径上的两条边(i, i+1)和(j, j+1),然后反转i+1到j之间的子路径。这相当于“解开交叉”。实现如下:
def generate_neighbor_2opt(self, solution): """使用2-opt移动产生邻域解。""" new_solution = solution.copy() n = self.num_cities # 随机选择两个不同的位置,确保 i < j i, j = sorted(random.sample(range(n), 2)) # 反转 i+1 到 j 之间的子路径 new_solution[i+1:j+1] = reversed(new_solution[i+1:j+1]) return new_solution为什么2-opt更有效?在平面TSP中,最优路径通常不会自相交。2-opt操作直接针对路径中的“交叉”进行消除,是一种“问题感知”的启发式扰动,比盲目交换更容易产生有意义的改进。在实际测试中,使用2-opt作为邻域动作,算法的收敛速度和解的质量通常有显著提升。
实操心得:邻域动作的设计是模拟退火乃至所有局部搜索算法的灵魂。一个好的邻域应该满足:1)可达性:从任何解出发,通过有限步邻域移动能到达任何其他解(保证理论上的全局搜索能力);2)相关性:新解与旧解在目标函数上不应差异过大或过小,保持一定的接受概率;3)计算效率:生成新解和计算其成本增量应尽可能快。对于TSP,2-opt在这三点上取得了很好的平衡。
4.3 成本增量计算:避免重复的全路径计算
在算法内循环中,我们需要频繁计算新解的成本。如果每次都用total_distance函数重新计算整个路径的长度,当城市数量很多时,这会成为性能瓶颈。注意到我们的邻域动作(如交换或2-opt)只改变了路径的局部结构,因此路径总长的变化(Δcost)可以只通过考察被改动的那几条边来计算。
以交换操作为例:假设交换了位置i和j的城市。那么路径中只有与城市i-1,i,i+1和j-1,j,j+1相关的边发生了变化(注意处理头尾相连)。我们可以只计算这些边改变前后的距离差,而不必重新计算整个路径。
以2-opt操作为例:反转了子路径[i+1 ... j]。受影响的边是原来的边(i, i+1)和(j, j+1)被移除,新增了边(i, j)和(i+1, j+1)。同时,子路径内部所有边的方向反转,但距离总和不变!因此Δcost的计算非常简单:
def delta_cost_2opt(self, solution, i, j): """计算对solution执行2-opt(i,j)操作带来的成本变化。""" n = self.num_cities # 获取四个相关城市在路径中的节点编号 a = solution[i] b = solution[(i+1) % n] c = solution[j] d = solution[(j+1) % n] # 旧边: (a-b) 和 (c-d) # 新边: (a-c) 和 (b-d) old_cost = self.dist_mat[a, b] + self.dist_mat[c, d] new_cost = self.dist_mat[a, c] + self.dist_mat[b, d] return new_cost - old_cost在solve方法的内循环中,我们可以先计算Δcost,再根据Metropolis准则决定是否接受。如果接受,再更新完整路径和总成本。这样可以将每次迭代的成本计算复杂度从O(n)降低到O(1),对于大规模问题(n>1000)是至关重要的优化。
5. 实战调试与常见问题排查
即使有了清晰的代码和理论,第一次运行时也可能遇到各种问题:比如算法根本不收敛,或者收敛到一个很差的解。下面是我在多次实践中总结的排查清单和调试技巧。
5.1 典型问题与解决方案速查表
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
| 成本始终不下降,在极高值波动 | 初始温度T_start过低 | 检查初始温度。使用estimate_initial_temperature方法估算,或大幅提高T_start(如1e4, 1e5)观察初期接受概率。 |
| 成本快速下降后陷入平台期,无法进一步优化 | 1. 降温过快(alpha太小) 2. 链长L不足 3. 邻域动作探索能力弱 4. 终止温度T_end过高 | 1. 增大alpha(如0.995->0.999),延长退火过程。 2. 增加L,或改为动态链长。 3. 尝试更强的邻域动作(如从“交换”改为“2-opt”)。 4. 降低T_end,让算法有更长的低温精细搜索时间。 |
| 每次运行得到的最优解差异很大 | 1. 随机性太强 2. 算法未充分收敛 | 1. 这是启发式算法的正常现象,可多次运行取最优。 2. 减缓降温速度,增加总迭代次数。考虑在低温阶段使用更贪婪的邻域动作。 |
| 算法运行时间过长 | 1. 城市数量n太大 2. 链长L或总迭代次数过多 3. 成本函数计算效率低 | 1. 对于超大n,考虑分层策略或使用更快的启发式算法初始化。 2. 调整参数,在质量和时间间权衡。可尝试自适应缩短链长。 3.务必实现增量成本计算,这是最大的性能优化点。 |
| 路径存在明显交叉 | 邻域动作不能有效消除交叉 | 确保使用2-opt或3-opt这类专门设计用于消除路径交叉的邻域动作。简单的交换操作对此无效。 |
5.2 调试技巧与可视化辅助
1. 监控关键指标:在算法运行时,除了记录最优成本,还应记录每个温度下的平均接受概率。理想的退火过程是:高温时接受概率接近1(广泛探索),随后缓慢下降,在低温时接近0(局部利用)。如果你的曲线不是这样,参数可能有问题。
# 在solve方法的内循环中增加记录 acceptances_at_T = 0 for _ in range(self.L): # ... 产生新解,计算delta ... if accept: acceptances_at_T += 1 # ... # 内循环结束后 accept_rate = acceptances_at_T / self.L print(f"温度 {T:.2f}, 接受率 {accept_rate:.3f}")2. 绘制更详细的搜索轨迹图:我们可以把每一次尝试(无论是否接受)的成本和温度都画出来,这能更直观地看到搜索空间的探索情况。
def plot_search_scatter(self): """绘制所有尝试过的解的成本散点图,用颜色表示温度。""" # 需要在solve方法中记录每次尝试的数据 # 假设我们记录了列表 attempt_costs 和 attempt_temps plt.figure(figsize=(10,6)) scatter = plt.scatter(range(len(self.attempt_costs)), self.attempt_costs, c=self.attempt_temps, cmap='viridis', s=1, alpha=0.6) plt.colorbar(scatter, label='温度') plt.xlabel('尝试次数') plt.ylabel('路径成本') plt.title('模拟退火搜索轨迹 (颜色表示温度)') plt.grid(True, alpha=0.3) plt.show()从这张图上,你可以看到高温区(暖色)的点分散很广(探索),低温区(冷色)的点聚集在低成本区域(利用)。
3. 与简单启发式算法对比:在运行模拟退火前,先用一个非常简单的算法(如最近邻贪心算法)求一个解作为基准。这有两个好处:一是可以作为模拟退火的初始解,加速收敛;二是可以对比结果,直观感受模拟退火的提升幅度。
def nearest_neighbor_solution(self, start_city=0): """最近邻贪心算法生成初始解。""" unvisited = set(range(self.num_cities)) unvisited.remove(start_city) solution = [start_city] current = start_city while unvisited: # 找到离当前城市最近的未访问城市 next_city = min(unvisited, key=lambda city: self.dist_mat[current, city]) solution.append(next_city) unvisited.remove(next_city) current = next_city return solution4. 参数敏感性分析:对于你的特定问题,可以写一个简单的脚本,对关键参数(T_start, alpha, L)进行网格搜索,观察不同参数组合下最终解的质量和运行时间,找到最适合你问题规模的“甜点”。
5.3 性能优化实战:增量计算集成
让我们将之前讨论的增量计算优化集成到主算法中,这是提升大规模问题性能的必做步骤。我们以2-opt邻域为例:
def solve_optimized(self): """使用增量成本计算和2-opt邻域的优化版本。""" self.current_solution = self.generate_initial_solution() self.current_cost = self.total_distance(self.current_solution) self.best_solution = self.current_solution.copy() self.best_cost = self.current_cost T = self.T_start iteration = 0 n = self.num_cities while T > self.T_end: accept_count = 0 for _ in range(self.L): # 随机选择两个位置进行2-opt邻域移动 i, j = sorted(random.sample(range(n), 2)) # 计算成本变化量 (增量计算) delta = self.delta_cost_2opt(self.current_solution, i, j) # Metropolis准则 if delta < 0 or random.random() < math.exp(-delta / T): # 接受移动 accept_count += 1 # 执行2-opt反转操作,并更新当前成本和路径 self.apply_2opt(self.current_solution, i, j) self.current_cost += delta # 增量更新成本 # 更新历史最优 if self.current_cost < self.best_cost: self.best_solution = self.current_solution.copy() self.best_cost = self.current_cost # 记录和降温 self.cost_history.append(self.current_cost) self.best_cost_history.append(self.best_cost) self.temperature_history.append(T) acceptance_rate = accept_count / self.L # 可以在这里根据acceptance_rate动态调整链长L或降温计划 T *= self.alpha iteration += 1 return self.best_solution, self.best_cost def apply_2opt(self, solution, i, j): """在solution上原地执行2-opt反转操作。""" # 反转 i+1 到 j 之间的子序列 start, end = i+1, j while start < end: solution[start], solution[end] = solution[end], solution[start] start += 1 end -= 1这个优化版本将每次迭代的成本计算从O(n)降到了O(1),对于成千上万个城市的问题,速度提升是数量级的。这是将算法从“玩具代码”变为“实用工具”的关键一步。
模拟退火算法就像一个经验丰富的探险家,它既不会因为眼前的小山丘(局部最优)而满足,也不会在广袤的地形(解空间)中完全迷失方向。通过控制“温度”这个参数,它在“大胆探索”和“小心求证”之间取得了精妙的平衡。实现它最大的乐趣在于,你能亲手调整这些“物理参数”,观察算法行为的变化,并最终为你手中的组合优化问题找到一个漂亮的近似解。上面的代码和技巧为你提供了一个坚实的起点,但别忘了,最好的参数和邻域设计永远来自于对你特定问题的深入理解和反复实验。