☰
多旅行商问题MTSP实战:遗传算法与模拟退火算法求解及可视化
2026/10/29 23:54:39 网站建设 项目流程

简介:本资源围绕多旅行商问题(MTSP)展开,分别用遗传算法与模拟退火算法实现求解,面向计算机、人工智能、自动化等专业学生及需要课程设计、毕设或算法进阶的学习者。项目包含两个实验场景:工厂巡检路线安排与运输机航线规划,数据分别存放于对应文本文件中,最终可视化结果在Jupyter Notebook中呈现。压缩包共15个文件,约3.26MB,涵盖Python源码、数据文本、Notebook、HTML结果页、PNG与GIF动图及说明文档,结构清晰,便于对照算法实现与实验效果。目前已有78人学习下载。读者可获取两套完整算法代码、实验数据与可视化结果,理解GA与SA在MTSP中的建模差异、参数设置与收敛表现,也可在此基础上修改扩展,用于课程作业、毕业设计或项目初期演示。

1. 从两份真实数据说起:这个 MTSP 资源包到底能跑出什么

多旅行商问题(MTSP)不是课本里那种只存在于论文里的抽象模型,它对应的是很具体的调度场景:多个执行体从同一个或不同的起点出发,各自走一条路线,把所有目标点覆盖完,最后让总路程或最大单程尽量小。这个资源包直接给了两个能跑的实验——工厂巡检路线和运输机航线,数据分别是data/routine_nodes.txt和data/provincial_capital.txt,算法侧同时实现了遗传算法和模拟退火算法,可视化结果落在mtsp_ga_and_sa.ipynb里。它适合正在做课程设计、毕设,或者需要快速验证 MTSP 求解效果的人。你拿到手不是一堆散代码,而是一套能对照实验、能改参数、能换数据的完整流程。

2. 遗传算法解 MTSP:编码、适应度与交叉变异的落地细节

2.1 为什么 MTSP 不能直接套用单旅行商的排列编码

单旅行商问题(TSP)最自然的编码是一个城市排列,比如[0, 3, 1, 4, 2],解码就是按顺序走一圈。但 MTSP 有多个旅行商,如果还是只用一个排列,就必须额外决定“在哪里切分给不同的人”。这个资源包里的genetic_algorithm.py采用的是分段式染色体:一条染色体仍然是一个完整排列,但额外维护一组断点,把排列切成若干段,每段对应一个旅行商的路线。这样做的好处是交叉和变异算子可以复用 TSP 的成熟逻辑,代价是断点位置也需要参与进化。

常见做法是固定断点数量等于旅行商数量,然后让断点在排列长度范围内随机生成并参与变异。这个项目里两个实验的旅行商数量不同,实验一工厂巡检通常是多组人员分区域,实验二运输机航线则对应多架飞机从不同省会出发。你在改数据时,第一件要确认的事就是旅行商数量这个参数在哪里设置,否则跑出来的路线数对不上你的实际业务。

2.2 适应度函数怎么写才不翻车

适应度函数直接决定算法往哪个方向收敛。MTSP 的优化目标通常有两种:最小化总路程,或者最小化最大单条路线长度。前者适合总成本敏感的场景,后者适合负载均衡敏感的场景。这个资源包的 GA 实现里,适应度是总路程的倒数加上一个惩罚项,惩罚那些路线长度严重不均衡的个体。

# genetic_algorithm.py 中适应度计算的核心逻辑(示意) def fitness(self, individual): routes = self.decode(individual) # 把染色体解码成多条路线 total_dist = sum(self.route_distance(r) for r in routes) max_dist = max(self.route_distance(r) for r in routes) # 惩罚不均衡:最大路线越长,惩罚越重 balance_penalty = self.alpha * max_dist return 1.0 / (total_dist + balance_penalty + 1e-6)

这里的alpha是均衡权重,调大它会让算法更倾向于把路程摊平,调小则更看重总路程最短。很多新手跑出来一条路线特别长、其他路线很短,就是因为alpha设得太小或者根本没设。我一般会先把alpha设成 0.1 左右跑一轮,看路线分布,再决定往哪个方向调。

2.3 交叉与变异算子的参数怎么设

GA 的核心参数是种群大小、交叉概率、变异概率和迭代次数。这个资源包里的默认值在genetic_algorithm.py的入口部分可以找到,通常是种群 100、交叉 0.8、变异 0.05、迭代 500。但默认值不一定适合你的数据规模。城市数量从 20 涨到 100 时,种群太小会导致早熟收敛,太大则单轮迭代变慢。

# 运行 GA 的典型调用方式 from genetic_algorithm import GeneticAlgorithm ga = GeneticAlgorithm( data_file='data/routine_nodes.txt', num_salesmen=3, # 旅行商数量 pop_size=120, # 种群规模 crossover_rate=0.85, # 交叉概率 mutation_rate=0.08, # 变异概率 generations=800, # 迭代代数 alpha=0.1 # 均衡惩罚权重 ) best_route, best_dist = ga.run() print('最优总路程:', best_dist)

参数说明:num_salesmen必须和你的业务场景一致;pop_size建议取城市数量的 2 到 5 倍;crossover_rate一般不低于 0.7,否则进化太慢;mutation_rate在 0.01 到 0.1 之间,太高会退化成随机搜索。迭代次数看收敛曲线,如果 300 代之后适应度基本不动了,加代数意义不大,不如调种群或算子。

2.4 用routine_nodes.txt跑通第一个实验

实验一的数据是工厂巡检节点,文件里每行是一个节点的坐标。运行前先确认文件路径和格式,然后直接调用 GA 入口。跑完之后,GA 会输出最优路线和总距离,同时把结果写进ga_best_all_air_line.html这样的可视化文件里。如果你在 notebook 里跑,mtsp_ga_and_sa.ipynb已经帮你把绘图逻辑串好了,直接执行对应 cell 就能看到路线图。

注意:routine_nodes.txt和provincial_capital.txt的坐标量纲可能不同,前者可能是平面坐标,后者是经纬度。如果你把两个数据混用,距离计算会失真。换数据时先检查utils.py里的距离函数用的是欧氏距离还是球面距离。

3. 模拟退火解 MTSP:邻域构造与降温策略的实操要点

3.1 SA 在 MTSP 里的状态表示和邻域动作

模拟退火算法解 MTSP 的思路和 GA 不同。SA 不维护种群,而是从一个初始解出发,通过邻域动作不断产生新解,按 Metropolis 准则决定是否接受。这个资源包的simulated_annealing.py里,状态表示同样是一条完整排列加断点,邻域动作主要有三类:交换两个城市、反转一段路线、移动断点位置。

这三类动作对应不同的搜索能力。交换城市适合微调局部顺序,反转路线适合打散长路径,移动断点则直接改变旅行商之间的任务分配。实际跑的时候,如果只做交换,解容易卡在局部最优;如果只做断点移动,路线内部顺序又优化不动。常见做法是给三类动作分配不同权重,比如交换 0.5、反转 0.3、断点移动 0.2。

3.2 降温系数和初始温度怎么定

SA 最玄学的两个参数就是初始温度和降温系数。初始温度太高,前期接受大量劣解,收敛慢;太低则一开始就陷入局部最优。这个资源包里初始温度是根据初始解的目标函数值估算的,降温系数默认在 0.95 到 0.99 之间。

# simulated_annealing.py 中的降温循环(示意) def anneal(self): current = self.initial_solution() best = current T = self.initial_temp while T > self.min_temp: for _ in range(self.iter_per_temp): neighbor = self.random_neighbor(current) delta = self.cost(neighbor) - self.cost(current) if delta < 0 or random.random() < math.exp(-delta / T): current = neighbor if self.cost(current) < self.cost(best): best = current T *= self.cooling_rate # 降温 return best

cooling_rate越接近 1,降温越慢,搜索越充分但耗时越长。iter_per_temp是每个温度下的迭代次数,太小会导致每个温度还没搜够就降下去了。我一般会把cooling_rate设在 0.97 左右,iter_per_temp取城市数量的 5 到 10 倍,先跑一轮看收敛曲线再微调。

3.3 用provincial_capital.txt跑通第二个实验

实验二的数据是省会城市坐标,对应运输机航线规划。这个数据的规模比实验一大,城市之间距离也更远,所以 SA 的初始温度和迭代次数都要相应调大。运行方式和 GA 类似,调用simulated_annealing.py的入口,指定数据文件和旅行商数量,跑完后结果会写入sa_best_all_air_line.html。

from simulated_annealing import SimulatedAnnealing sa = SimulatedAnnealing( data_file='data/provincial_capital.txt', num_salesmen=4, initial_temp=1000, cooling_rate=0.97, iter_per_temp=200, min_temp=1e-3 ) best_route, best_dist = sa.run() print('SA 最优总路程:', best_dist)

参数说明:initial_temp可以先用 1000 试,如果前期接受率低于 0.5 就调高;cooling_rate不要低于 0.9,否则退火太快;min_temp是终止温度,设到 1e-3 量级就够了。跑完 SA 之后,建议和 GA 的结果对比一下,两个算法在同一数据上的表现差异能帮你判断参数是否合理。

3.4 GA 和 SA 在同一数据上的对比方法

这个资源包的价值之一就是让你能直接对比两种算法。mtsp_ga_and_sa.ipynb里已经把两个实验的 GA 和 SA 结果放在一起可视化。你可以在 notebook 里分别跑两个算法,记录总路程、最大单条路线长度和运行时间,然后画成对比图。

对比时要注意:GA 是种群搜索,单次运行结果波动较大,最好跑 5 次取平均;SA 是单点搜索,结果受初始解影响明显,可以换几个随机种子各跑一次。如果 GA 的平均结果明显优于 SA,可能是 SA 的降温策略太激进;如果 SA 反超 GA,检查 GA 的种群大小和变异率是不是设得太保守。

4. 避坑与排查:跑 MTSP 代码时最容易翻车的五个地方

4.1 现象:路线数量对不上旅行商数量

原因:num_salesmen参数没有正确传入,或者断点生成逻辑在数据量变化时越界了。解决:先打印解码后的路线列表长度,确认等于num_salesmen;如果不等,检查断点生成函数是否用了硬编码的索引范围。

4.2 现象:GA 跑了很久适应度不涨

原因:种群多样性过早丢失,或者变异率太低导致新解产生不足。解决:把mutation_rate从 0.01 提到 0.05 以上,同时检查交叉算子是否真的在交换有效片段。如果还是不动,把种群大小翻倍再试。

4.3 现象:SA 结果每次差异特别大

原因:初始温度或降温系数设置不合理,导致搜索过程不稳定。解决:固定随机种子跑几次,看结果方差;如果方差过大,把cooling_rate调高到 0.98 以上,并增加iter_per_temp。

4.4 现象:可视化 HTML 打开是空白

原因:绘图数据没有正确写入,或者浏览器拦截了本地文件。解决:先检查 notebook 里绘图 cell 是否执行成功,再看 HTML 文件大小是否正常。如果文件很小,说明数据没写进去,回到代码里检查结果保存路径。

4.5 现象:换了自己的数据后距离计算明显不对

原因:坐标量纲和距离函数不匹配,比如经纬度用了欧氏距离。解决:打开utils.py,确认距离函数是euclidean还是haversine,根据你的数据格式切换。如果是经纬度,必须用球面距离,否则结果没有参考价值。

5. 进阶用法:把 MTSP 求解器接进你自己的调度流程

这个资源包最实用的进阶方向,是把 GA 或 SA 的求解结果导出成结构化数据,再接进你自己的排班或路径规划流程。mtsp_ga_and_sa.ipynb里已经有把路线写成 HTML 的逻辑,但 HTML 不方便程序消费。我一般会加一个导出 JSON 的函数,把每条路线的节点顺序、总距离、单条距离都存下来。

import json def export_routes(routes, distances, filename='routes.json'): result = [] for i, (route, dist) in enumerate(zip(routes, distances)): result.append({ 'salesman_id': i, 'nodes': route, 'distance': dist }) with open(filename, 'w', encoding='utf-8') as f: json.dump(result, f, ensure_ascii=False, indent=2) print(f'已导出 {len(result)} 条路线到 {filename}')

导出之后,你可以用这个 JSON 去驱动地图 API 画线,或者喂给排班系统做进一步约束校验。另一个进阶方向是给 GA 加精英保留策略:每代把最好的几个个体直接复制到下一代,防止最优解在交叉变异中被破坏。这个改动很小,但在 MTSP 上通常能明显提升收敛稳定性。

验证方法上,我习惯用两个指标交叉检查:一是总路程,二是最大单条路线长度。如果总路程降了但最大单条路线反而涨了,说明算法在牺牲均衡性换总距离,这时候要回头调alpha或 SA 的邻域权重。从那以后我每次换数据或改参数,都强制走一遍“先跑小规模看路线分布,再跑全量看收敛曲线”的流程,避免直接上全量数据跑半天才发现参数不对。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询