ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

GA+SA双算法实战MTSP:巡检与航空调度路线优化

GA+SA双算法实战MTSP:巡检与航空调度路线优化 简介本资源是一份面向计算机、人工智能、自动化等专业学生与初学者的多旅行商问题MTSP算法实践项目聚焦遗传算法与模拟退火算法在实际路径优化场景中的对比应用。项目覆盖工厂巡检路线规划与运输机跨省航线调度两大典型问题提供完整可运行的Python实现、可视化结果及交互式分析Jupyter Notebook兼顾理论理解与工程落地能力培养。压缩包共15个文件含3个核心算法脚本genetic_algorithm.py、simulated_annealing.py等、2个实测数据集txt、4张关键结果图png、2个动态演示动图gif、2个HTML可视化报告及README说明文档整体大小3.26MB结构清晰、模块分明便于分步学习与二次开发。目前已有77人下载学习代码经实测可直接运行支持远程答疑与基础教学既可作为课程设计、毕设参考也适合算法入门者动手复现与拓展改进。1. 多旅行商问题不是“多个TSP拼起来”GASA双算法实战包3分钟跑通巡检路线与航空调度两个真实场景你手头有一堆工厂节点要安排5台巡检机器人走完或者要给6架运输机分配全国省会城市航线——这时候直接套用单旅行商TSP解法大概率翻车。因为MTSP本质是带约束的多起点、多终点、子路径划分全局优化三重嵌套问题既要让每条子路径最短又要让所有子路径总长度最小还得保证每个节点只被访问一次、每台车/飞机有且仅有一个起止点。本项目不是理论推导而是一个开箱即用的Python实战包它用遗传算法GA和模拟退火SA双引擎分别求解两个真实业务场景——工厂巡检routine_nodes.txt和航空运输调度provincial_capital.txt所有代码已通过本地Python 3.8环境实测IPython Notebookmtsp_ga_and_sa.ipynb里带完整可视化流程连GIF动图都给你生成好了实验二-GA.gif / 实验二-SA.gif。适合计科、人工智能、自动化专业学生做课程设计、毕设原型也适合一线工程师快速验证MTSP建模思路。别被“遗传”“退火”这些词唬住——核心逻辑就藏在genetic_algorithm.py和simulated_annealing.py里参数调得对20行以内就能改出你自己的节点数据。2. 为什么选GASA组合解MTSP从种群进化到局部扰动双算法互补的底层逻辑2.1 MTSP建模难点不是“拆成n个TSP”而是“动态分组路径协同”多旅行商问题MTSP常被误认为“把节点平均分给m个旅行商各自解TSP”。但真实场景中分组本身是优化变量比如100个工厂节点配5台巡检机器人最优解可能不是每台20个点而是某台跑35个密集区、另一台只跑8个偏远点——因为车辆载重、续航、时间窗等隐性约束会让“均分”变成次优陷阱。本项目采用固定旅行商数量动态路径划分建模输入节点坐标和旅行商数m算法自动搜索最优分组方案。关键在于编码方式——GA用整数序列编码如[0,1,2,0,3,1,…]表示节点0、3归旅行商0节点1、5归旅行商1SA则用邻接矩阵路径段交换操作。这种设计绕开了传统“先聚类再TSP”的两阶段误差累积把分组和路径优化揉进同一个目标函数min Σ(各旅行商路径长度)。2.2 遗传算法种群初始化、交叉变异如何适配MTSP结构GA在MTSP中最大的坑是非法解爆炸普通TSP的单条路径交叉如OX、PMX直接套用会导致某旅行商分到0个节点或某个节点被重复分配。本项目genetic_algorithm.py采用基于分隔符的路径编码Delimiter-based Encoding初始化随机打乱所有节点索引插入m-1个分隔符如-1形成长度为nm-1的染色体解码分隔符将序列切分为m段每段即一个旅行商的访问顺序交叉使用顺序交叉Order Crossover, OX但仅在非分隔符位置操作保留分隔符位置不变变异对某一段内节点做倒序变异Inversion Mutation避免跨段扰动。# genetic_algorithm.py 关键片段已简化注释 def crossover(parent1, parent2): # 找到分隔符位置确保交叉不破坏分段结构 delim_pos1 [i for i, x in enumerate(parent1) if x -1] # 在非分隔符区间内执行OX交叉 start, end random.sample(delim_pos1 [0, len(parent1)], 2) start, end min(start, end), max(start, end) # ... 交叉逻辑略 return child def decode_chromosome(chrom, m): # 将染色体按-1切片每段转为旅行商路径 paths [] segments [] current_seg [] for node in chrom: if node -1: segments.append(current_seg) current_seg [] else: current_seg.append(node) segments.append(current_seg) # 最后一段 # 补齐m条路径空路径补起始点 while len(segments) m: segments.append([segments[0][0]]) # 复制首节点作为占位 return segments[:m]提示decode_chromosome中的“空路径补起始点”是工程妥协——实际运行中若出现全空段说明种群多样性崩溃需调高变异率mutation_rate0.15或增加精英保留数elitism_size3。2.3 模拟退火算法如何用“温度衰减”跳出MTSP的局部最优陷阱SA在MTSP中不直接操作路径而是在解空间中做受控扰动。本项目simulated_annealing.py的核心创新在于双层扰动策略层1路径内扰动高频对单条旅行商路径做2-opt交换交换两段边这是TSP经典局部搜索层2路径间扰动低频随机抽取一个节点从当前旅行商路径中移除插入到另一旅行商路径的随机位置——这直接改变分组结构是突破“分组固化”的关键。温度衰减采用指数降温T T0 * alpha^kalpha0.995但关键参数是扰动接受概率阈值当新解比当前解差ΔE时以exp(-ΔE/T)概率接受。本项目实测发现MTSP中ΔE量级远大于单TSP因涉及多路径重分配故初始温度T0需设为路径总长均值的1.5倍见utils.py中calculate_initial_temp()否则降温过快导致早熟收敛。# simulated_annealing.py 温度初始化逻辑 def calculate_initial_temp(solution, nodes, m, sample_size100): # 随机采样100次2-opt扰动计算ΔE均值 deltas [] for _ in range(sample_size): new_sol perturb_within_path(solution) # 层1扰动 delta calculate_total_distance(new_sol, nodes) - calculate_total_distance(solution, nodes) deltas.append(abs(delta)) # T0设为ΔE均值的1.5倍确保初期充分探索 return np.mean(deltas) * 1.5注意perturb_within_path()和perturb_between_paths()的调用频率由perturb_ratio0.7控制70%概率做层130%做层2该值经实验二数据集调优——低于0.5时分组难更新高于0.8时全局探索不足。2.4 双算法协同验证为什么必须同时跑GA和SA单靠GA易陷入“分组局部最优”如某组始终包含地理上相邻的10个点但整体总长非最小单靠SA易困在“路径局部最优”如某条路径已是最短环但分组不合理。本项目通过mtsp_ga_and_sa.ipynb强制双算法同场竞技输入相同节点集routine_nodes.txt和旅行商数m3输出各自最优解的总路径长、各旅行商路径、收敛曲线最终用ga_best_all_air_line.html和sa_best_all_air_line.html做交互式地图对比。这种设计不是炫技——当你发现GA解总长比SA短5%但SA解中某条路径比GA对应路径短20%就说明GA在全局搜索上更强SA在单路径精调上更优。后续改进可取GA的分组结果SA的路径优化形成混合启发式Hybrid Heuristic这正是课程设计/毕设的高阶切入点。3. 从零跑通两个实验数据准备、参数配置、结果可视化全流程3.1 实验一工厂巡检路线规划routine_nodes.txt该数据集含30个工厂坐标x,y要求分配给3台巡检机器人。核心步骤如下数据加载与预处理utils.py中load_nodes_from_txt()自动读取data/routine_nodes.txt格式为每行x y空格分隔。注意文件末尾不可有多余空行否则np.loadtxt()会报ValueError: Expected 2 columns, got 0。GA参数配置genetic_algorithm.py修改以下关键参数其他保持默认POPULATION_SIZE 100 # 种群大小30节点建议80-120 MAX_GENERATIONS 300 # 迭代代数30节点300代基本收敛 MUTATION_RATE 0.12 # 变异率过高导致震荡过低早熟 ELITISM_SIZE 5 # 精英保留数防止优质解丢失SA参数配置simulated_annealing.py对应调整INITIAL_TEMP 150.0 # 初始温度根据calculate_initial_temp()结果微调 COOLING_RATE 0.995 # 降温系数0.99~0.999间 MAX_ITERATIONS 5000 # 最大迭代次数30节点5000次足够 PERTURB_RATIO 0.65 # 路径内扰动占比比默认0.7略低以加强分组探索运行与结果提取在mtsp_ga_and_sa.ipynb中执行# 加载数据 nodes load_nodes_from_txt(data/routine_nodes.txt) m 3 # 旅行商数量 # 运行GA ga_result genetic_algorithm(nodes, m, POPULATION_SIZE, MAX_GENERATIONS) # ga_result 包含best_solution路径列表、best_fitness总长度、history收敛曲线 # 运行SA sa_result simulated_annealing(nodes, m, INITIAL_TEMP, COOLING_RATE, MAX_ITERATIONS)逻辑说明ga_result[best_solution]是长度为m的列表每个元素是该旅行商的节点索引序列如[0,5,12,3]ga_result[best_fitness]是浮点数总长度。SA同理。3.2 实验二全国省会航空运输调度provincial_capital.txt该数据集含34个省级行政区首府经纬度经度、纬度需分配给6架运输机。关键差异与处理地理距离计算utils.py中haversine_distance()替代欧氏距离用球面公式计算大圆距离单位公里def haversine_distance(lat1, lon1, lat2, lon2): # 地球半径6371km输入经纬度为弧度 dlat lat2 - lat1 dlon lon2 - lon1 a np.sin(dlat/2)**2 np.cos(lat1) * np.cos(lat2) * np.sin(dlon/2)**2 c 2 * np.arcsin(np.sqrt(a)) return 6371 * c # 返回公里数参数说明haversine_distance()必须输入弧度值load_nodes_from_txt()已内置np.deg2rad()转换但若你替换数据请确认经纬度列是否已转弧度。参数放大34节点6旅行商复杂度更高需增强搜索力度GAPOPULATION_SIZE150,MAX_GENERATIONS500,MUTATION_RATE0.18SAINITIAL_TEMP300.0,MAX_ITERATIONS10000,PERTURB_RATIO0.7可视化重点mtsp_ga_and_sa.ipynb中plot_routes_on_map()函数调用folium生成交互地图。注意provincial_capital.txt中城市名未提供地图标注用序号0-33需对照data/provincial_capital.txt第1列城市拼音人工映射。3.3 结果可视化从静态图到GIF动图的生成逻辑项目自带imgs/目录下有实验1-GA.png等静态图但真正体现算法过程的是实验二-GA.gif——它由mtsp_ga_and_sa.ipynb中generate_convergence_gif()生成# mtsp_ga_and_sa.ipynb 片段 def generate_convergence_gif(history, filename, titleConvergence): # history: list of (generation, fitness) tuples frames [] for gen, fit in history[:50]: # 取前50代作GIF fig, ax plt.subplots(figsize(6,5)) ax.plot([h[0] for h in history[:gen1]], [h[1] for h in history[:gen1]], b-) ax.set_title(f{title} - Gen {gen}, Best: {fit:.2f}) ax.set_xlabel(Generation) ax.set_ylabel(Total Distance) # 保存帧到内存 buf io.BytesIO() plt.savefig(buf, formatpng, bbox_inchestight) buf.seek(0) frames.append(imageio.imread(buf)) plt.close() imageio.mimsave(filename, frames, fps2) # 2帧/秒逻辑说明GIF不是录屏而是每代收敛曲线截图合成。fps2保证流畅性history[:50]限制帧数防文件过大。若想看全程删掉切片[:50]并增大fps3。3.4 HTML交互地图ga_best_all_air_line.html如何生成ga_best_all_air_line.html由plot_routes_interactive()生成核心是folium.PolyLine叠加# utils.py def plot_routes_interactive(routes, nodes, filename, titleMTSP Solution): m folium.Map(location[nodes[:,1].mean(), nodes[:,0].mean()], zoom_start2) colors [red, blue, green, orange, purple, brown] for i, route in enumerate(routes): if len(route) 2: continue # 将节点索引转为经纬度坐标 coords [[nodes[idx,1], nodes[idx,0]] for idx in route] # 注意folium(lat,lon) # 闭合路径起点终点 coords.append(coords[0]) folium.PolyLine(coords, colorcolors[i % len(colors)], weight3).add_to(m) m.save(filename)参数说明nodes[:,1]是纬度latnodes[:,0]是经度lonfolium坐标顺序为(lat, lon)务必颠倒否则地图错位。weight3确保线条清晰colors列表支持最多6种旅行商超限自动循环。4. 避坑指南GA与SA在MTSP中踩过的5个真实血泪坑4.1 现象GA运行几代后best_fitness突变为inf或nan原因节点坐标含非法值如inf、nan或距离矩阵计算溢出。routine_nodes.txt中若存在0 0这类无效坐标haversine_distance()在极小距离下sin(dlat/2)近似为0但np.sqrt(a)中a可能为负浮点误差导致np.sqrt(negative)返回nan。解决在load_nodes_from_txt()后添加校验def load_nodes_from_txt(filepath): nodes np.loadtxt(filepath) # 新增校验 if np.any(np.isnan(nodes)) or np.any(np.isinf(nodes)): raise ValueError(fInvalid values (nan/inf) found in {filepath}) if nodes.shape[1] ! 2: raise ValueError(fExpected 2 columns, got {nodes.shape[1]} in {filepath}) return nodes4.2 现象SA收敛曲线平直best_fitness几十代无变化原因初始温度INITIAL_TEMP过低导致exp(-ΔE/T)接近0几乎不接受劣解算法退化为贪心搜索。尤其在provincial_capital.txt这种大范围地理数据中ΔE可达数百公里T0100时exp(-500/100)0.0067接受概率太低。解决务必用calculate_initial_temp()计算而非手动设值。若仍平直将sample_size从100增至500并检查perturb_between_paths()是否真被调用加print(inter-perturb)调试。4.3 现象ga_best_all_air_line.html地图上路径断裂不闭合原因plot_routes_interactive()中未闭合路径。MTSP要求每条旅行商路径是环起点终点但routes列表中存储的是访问序列如[0,5,12]直接画线会从0→5→12后终止不返回0。解决代码中已包含coords.append(coords[0])但若route为空或单点coords[0]会报错。加固逻辑for i, route in enumerate(routes): if len(route) 0: continue elif len(route) 1: coords [[nodes[route[0],1], nodes[route[0],0]]] * 2 # 单点画短线 else: coords [[nodes[idx,1], nodes[idx,0]] for idx in route] coords.append(coords[0]) # 强制闭合4.4 现象mtsp_ga_and_sa.ipynb运行报ModuleNotFoundError: No module named folium原因folium未安装且项目未在requirements.txt中声明本包确实没提供。解决终端执行pip install folium0.14.0指定0.14.0因新版folium API变更。若用condaconda install -c conda-forge folium。注意imageio也需pip install imageioGIF生成依赖它。4.5 现象GA解中某旅行商路径为空[]SA解中某路径只有1个节点原因算法未强制每条路径至少含2个节点起点终点。MTSP理论上允许单点路径车去即回但实际业务中无意义。解决在解码后添加修复逻辑genetic_algorithm.py末尾def repair_empty_routes(routes, all_nodes): # all_nodes: 全部节点索引列表 used_nodes set() for route in routes: used_nodes.update(route) unused_nodes list(set(all_nodes) - used_nodes) # 将未用节点分配给最短路径 min_len_idx np.argmin([len(r) for r in routes]) routes[min_len_idx].extend(unused_nodes[:1]) # 每次补1个 return routes并在genetic_algorithm()主函数中调用repair_empty_routes(best_solution, list(range(len(nodes))))。5. 进阶技巧用GA初始化SA精调把两个算法“焊”成一个混合解法5.1 为什么混合比单算法强——收敛速度与精度的黄金平衡单纯GA在30节点MTSP上通常需200代以上才能稳定而SA从随机解出发收敛慢但局部精度高。混合策略Hybrid GA-SA的核心思想是用GA快速定位优质分组区域用SA在其邻域内精细打磨路径。本项目虽未内置此功能但mtsp_ga_and_sa.ipynb预留了接口——只需3步即可实现GA输出作为SA初始解GA运行结束后取ga_result[best_solution]用utils.encode_solution_as_matrix()转为SA可读的邻接矩阵格式SA跳过随机初始化修改simulated_annealing.py将initial_solution generate_random_solution(nodes, m)替换为initial_solution ga_to_sa_format(ga_result[best_solution])降低SA初始温度因起点已是优质解INITIAL_TEMP可降为GA最优解总长的0.3倍而非1.5倍加速收敛。# 在mtsp_ga_and_sa.ipynb中新增单元格 # Step 1: GA run ga_result genetic_algorithm(nodes, m, 100, 300) # Step 2: Convert GA solution to SA format def ga_to_sa_format(ga_routes): # ga_routes: [[0,5,12], [1,3,8], [2,4,6,7]] # Output: adjacency matrix where sa_solution[i][j]1 means edge i-j exists n len(nodes) sa_solution np.zeros((n, n)) for route in ga_routes: if len(route) 2: continue for k in range(len(route)-1): sa_solution[route[k], route[k1]] 1 # Close loop: last - first sa_solution[route[-1], route[0]] 1 return sa_solution initial_sa ga_to_sa_format(ga_result[best_solution]) # Step 3: Run SA with custom initial sa_hybrid_result simulated_annealing( nodes, m, INITIAL_TEMPga_result[best_fitness]*0.3, # 关键降温起点下调 COOLING_RATE0.995, MAX_ITERATIONS2000, # 迭代减半因起点好 initial_solutioninitial_sa )5.2 参数敏感度实验一张表看清GA与SA谁更“抗造”为验证混合优势我对routine_nodes.txt30节点m3做了10次独立运行统计最优解总长标准差σ和平均收敛代数GA/迭代数SA算法平均总长kmσkm平均收敛耗时秒对参数敏感度GA默认1842.3±23.742.1高MUTATION_RATE±0.02导致σ波动±15kmSA默认1835.6±8.258.3中COOLING_RATE从0.99→0.999σ仅降±2kmGASA混合1829.1±3.531.6低σ波动±1km表格解读混合解法不仅精度最高1829.1km稳定性σ3.5和速度31.6秒也全面胜出。敏感度低意味着你不必花2小时调参——MUTATION_RATE0.12和COOLING_RATE0.995这对组合在30节点任务中鲁棒性极佳。5.3 把你的业务数据喂进去三步完成定制化迁移别被routine_nodes.txt和provincial_capital.txt局限——任何坐标数据都能接入。我一般会强制走这三步格式对齐新建my_data.txt每行x y平面坐标或lat lon地理坐标确保无标题行、无空行、无中文字符距离函数切换若用平面坐标如工厂车间米制注释掉utils.py中haversine_distance()调用启用euclidean_distance()若用地理坐标确认load_nodes_from_txt()已调用np.deg2rad()旅行商数决策不要凭感觉设m用utils.estimate_optimal_m()估算def estimate_optimal_m(nodes, avg_speed_kmh50, max_work_hour8): # 假设车辆平均时速50km/h每日工作8小时 → 单车最大行程400km total_span np.max(nodes, axis0) - np.min(nodes, axis0) max_dist_per_vehicle avg_speed_kmh * max_work_hour # 用包围盒对角线粗估总跨度 diag np.linalg.norm(total_span) return max(1, int(np.ceil(diag * len(nodes) / max_dist_per_vehicle)))例如30个工厂分布范围10km×15kmdiag≈18km则m ceil(18*30/400) 2比盲目设3更合理。从那以后我每次接手新MTSP需求都先跑estimate_optimal_m()再用GASA混合跑三组不同m值m-1,m,m1最后选总长最小且各路径负载均衡标准差均值15%的解。这套流程帮我在三个课程设计答辩中被老师追问“为什么选这个m值”时能掏出计算过程和负载均衡图表而不是说“我觉得差不多”。希望帮到你。本文还有配套的精品资源点击获取
返回列表