Python遗传算法求解带时间窗车辆路径问题(VRPTW)
简介面向物流优化、运筹学课程设计与遗传算法入门者的 Python 实战项目围绕带时间窗的车辆路径问题VRPTW给出一套可运行的遗传算法求解思路。内容涵盖客户点与时间窗、车辆容量等建模要素以及种群初始化、适应度评估、选择、交叉、变异和终止条件等完整流程并配有主程序、问题定义、算法实现与解输出等模块便于理解整数编码和路线优化。压缩包约 671KB文件总数显示为 0文件类型明细暂无有效数据下载后需按实际目录核对。已有 1583 人学习适合希望把遗传算法落到物流调度案例中的读者参考可据此梳理 VRPTW 约束处理、适应度设计和参数调试方法并在此基础上改造数据与目标函数。1. 从一张排班表说起带时间窗的车辆路径问题到底难在哪某配送团队手里有一个仓库、二十五个客户点、三台车每个客户都写明了「上午九点到十一点之间必须送到」早到要等、晚到要罚。第一版排线用的是最近邻贪心从仓库出发每次挑距离最近的那个点。跑到第七个客户就崩了前面为了省距离绕了一大圈到这里已经过了十一点后面所有点的时间窗跟着连锁失效整趟作废。这就是 VRPTW带时间窗的车辆路径问题和普通车辆路径问题最大的区别——距离不再是唯一成本时间是一条硬约束。用 Python 写一套遗传算法去解它代码量并不吓人真正决定结果能不能用的是三件事约束怎么表达、违约怎么罚、解怎么解码。这也是后面几章要一层层拆开的东西。2. 问题建模把时间窗、载重和服务时长写成可计算的数据契约建模这一步很多人跳得快觉得反正后面要写算法先把数据塞进列表跑起来再说。结果跑到一半发现时间单位有的是分钟有的是小时有的客户点有服务时长有的没写距离矩阵行列对不上调半天调不出问题在哪。我一般的做法是先把数据结构定死让后面所有算子都只依赖这份契约改一处不用动全身。这一章把客户点、车辆、约束检查和目标函数四块拆开写每一块都给出可以直接拿去用的 Python 结构。2.1 客户点与车辆的数据契约客户点要携带的信息比想象中多坐标决定距离需求量决定装载时间窗决定到达早晚服务时长决定什么时候能离开。仓库本质上也是一个客户点只是需求为 0、服务时长为 0、时间窗放宽到全时段所以直接沿用同一个结构编号固定为 0后面所有距离矩阵、路径拼接都靠这个约定省掉大量特殊判断。from dataclasses import dataclass dataclass(frozenTrue) class Customer: idx: int # 客户编号0 固定留给仓库 x: float # 平面坐标 x y: float # 平面坐标 y demand: float # 需求量单位与车辆容量保持一致 ready: float # 时间窗最早开始服务时刻 due: float # 时间窗最晚开始服务时刻 service: float # 服务时长 dataclass(frozenTrue) class Instance: depot: Customer # 仓库idx 必须为 0 customers: list # 客户列表不含仓库 vehicle_cap: float # 单车最大载重 vehicle_num: int # 可用车辆数上限 speed: float 1.0 # 单位时间行驶距离1.0 表示距离即时间用frozenTrue不是为了炫技是为了防止某个算子在迭代过程中顺手改了客户坐标或者时间窗最后结果对不上还找不到原因——这种黑匣子式的 bug 排查起来最耗时间不如从一开始就让数据不可变。speed这个字段看着多余实际很有用如果距离矩阵用的是公里、时间窗用的是分钟那么speed就填「每分钟行驶多少公里」整个模型的时间单位一次性统一不用在每个函数里各写一套换算。vehicle_num是上限而不是必须用满这一点很关键。有的方案把车辆数写死成 3结果算出来发现 3 台车根本装不下所有货程序要么报错要么给出一个超载解。写成上限之后解码阶段可以少开一辆车多开一辆也不会越界目标函数里再用固定成本去压制车辆数。2.2 单趟路线的可行性检查载重、等待、迟到三件事整个算法里被调用次数最多的函数就是这个。给定一条路线上的客户顺序模拟车辆从仓库出发、依次服务、回到仓库的全过程同时算出行驶距离、超载量和超时量。它必须足够快因为遗传算法一次运行要调用几万到几十万次。def check_route(route, inst, dist): 按顺序服务 route 中的客户返回该趟的可行性明细。 route: 客户下标列表不含仓库 返回: (是否可行, 行驶距离, 超载量, 超时量, 回仓时刻) load 0.0 clock 0.0 # 车辆离开仓库的时刻 prev 0 # 0 表示仓库 travel 0.0 late 0.0 for cid in route: c inst.customers[cid] # 到达时刻 上一站离开时刻 路上耗时 clock dist[prev][cid] / inst.speed if clock c.ready: clock c.ready # 早到必须等待等待本身不计成本 if clock c.due: late clock - c.due # 晚到的量累计成违约单位是时间 clock c.service # 服务完成后才能走 load c.demand travel dist[prev][cid] prev cid travel dist[prev][0] clock dist[prev][0] / inst.speed over max(0.0, load - inst.vehicle_cap) feasible (over 0.0) and (late 0.0) return feasible, travel, over, late, clock几个容易写错的地方值得单独点出来。第一等待时间的处理早到不能直接前进到下一站必须把时钟拨到ready再开始服务否则后面所有客户的到达时间都会算早看起来很美好实际全是假的。第二回仓时刻也要算进去因为如果车辆有最晚回仓限制比如司机下班时间这一段漏掉就会给出一个在实际业务里根本执行不了的排班。第三超时量记的是「晚到多少时间」而不是「晚了几个客户」因为晚 5 分钟和晚 50 分钟在业务上的严重程度完全不同用计数会把这个差异抹平。提示late累加的是超出due的时长不包括等待时间。有的实现把等待也计入成本那属于另一套问题设定改之前先确认业务方到底在意什么。2.3 目标函数距离、车辆数与两层违约的加权有了单趟检查整条染色体就能拆成若干趟分别评估再汇总。目标函数一般写成四项加权总行驶距离、启用的车辆数、超载总量、超时总量。前两项是真实成本后两项是违约惩罚理想状态下最优解的违约项应该归零。def objective(chrom, inst, dist, w(1.0, 80.0, 200.0, 200.0)): w (距离权重, 车辆固定成本, 超载罚系数, 超时罚系数) routes decode(chrom, inst, dist) # 见 3.1 的切分解码 total_dist over_sum late_sum 0.0 for r in routes: ok, d, over, late, _ check_route(r, inst, dist) total_dist d over_sum over late_sum late cost (w[0] * total_dist w[1] * len(routes) w[2] * over_sum w[3] * late_sum) return cost, total_dist, len(routes), over_sum, late_sum权重的取法有个经验值距离项的权重定成 1车辆固定成本取 50 到 100 之间大致相当于一辆车跑一趟的固定开销折算成距离两个罚系数取到距离项的 200 倍以上。罚系数太小的后果是算法会发现「超载一点点换来的距离节省更划算」最后收敛到一堆不可行解上跑得还挺开心。罚系数太大也有问题搜索前期几乎所有个体都被罚成天文数字适应度差异被压平选择压力消失等于随机乱走。注意罚系数不是越大越好。我一般在种群初始化之后先跑一代看可行解占比低于 30% 就把罚系数往上抬一档高于 80% 就往下压一档让可行解和非可行解都有存活空间。2.4 标准算例文件读入自己造数据容易把规模造得太温和跑出来的结果没有说服力。行业里通用的标准算例文件是纯文本格式头部写车辆数和容量中间是客户表每行依次是编号、x 坐标、y 坐标、需求量、最早服务时刻、最晚服务时刻、服务时长。写一个解析函数把这份文本转成上面的Instance后面换数据只需要换文件路径。def load_benchmark(path): 读取标准算例文本返回 Instance。跳过表头和空行。 with open(path, encodingutf-8) as f: lines [ln.strip() for ln in f if ln.strip()] vehicle_num vehicle_cap None customers, depot [], None in_table False for ln in lines: parts ln.split() if parts[0] VEHICLE: vehicle_num int(parts[1]) vehicle_cap float(parts[2]) elif parts[0].isdigit() and len(parts) 7: idx int(parts[0]) c Customer(idxidx, xfloat(parts[1]), yfloat(parts[2]), demandfloat(parts[3]), readyfloat(parts[4]), duefloat(parts[5]), servicefloat(parts[6])) if idx 0: depot c else: customers.append(c) assert depot is not None and vehicle_cap is not None, 算例格式不对 return Instance(depotdepot, customerscustomers, vehicle_capvehicle_cap, vehicle_numvehicle_num)解析函数里最稳妥的做法是用parts[0].isdigit()判断是否进入数据区而不是去匹配表头文字。不同来源的算例文件表头写法差异很大有的是CUST NO.有的是Customer硬匹配表头迟早翻车。坐标转距离矩阵时用欧氏距离就够注意保留浮点别提前取整取整造成的误差会随着路线长度累积最后对不上参考解。到这里问题已经变成一串数字和一个可计算的成本函数接下来的事情才是遗传算法要处理的。3. 遗传算法核心算子编码、交叉、变异三件套怎么配合遗传算法解 VRPTW 最容易踩的坑是把「染色体」直接写成一个二维结构——第几辆车跑哪几个客户。这样写看起来直观交叉的时候两父代按车辆切切完发现车辆数对不上、客户重复、客户丢失光写修复逻辑就能写三百行还修不干净。我更倾向另一种表示染色体就是一条包含所有客户的一维排列车辆切分放在解码阶段做。这样交叉变异永远只在一维排列上操作天然满足「每个客户恰好出现一次」可行性问题被推迟到一个统一的入口处理。3.1 用「大回路 切分解码」绕开不可行解这条一维排列业内叫 giant tour字面意思就是「把整个配送网络拉成一条大回路」。解码时从前往后扫描能塞进当前车就塞塞不下就新开一辆。判断「塞不下」的标准就是 2.2 里的check_route超载或者时间窗失效都算塞不下。def decode(chrom, inst, dist, max_vehiclesNone): 把客户排列切分成若干条可行路线。 chrom: 客户下标的一维排列 max_vehicles: 允许的最大车辆数None 表示用实例里的上限 limit max_vehicles or inst.vehicle_num routes, cur [], [] for cid in chrom: trial cur [cid] ok, *_ check_route(trial, inst, dist) if ok: cur trial else: if cur: # 当前车装不下了先封车 routes.append(cur) cur [cid] if len(routes) limit: # 车辆数已经用满剩余客户强行并入最后一车 routes[-1].extend([cid]) cur [] break if cur: routes.append(cur) return routes这段代码有两个设计取舍值得说。一是「贪心切分」扫到装不下才封车简单但不够优有时候提前一站封车让下一辆车从更靠前的客户开始整体距离反而更短。想做得更好可以加一步局部搜索在切分点附近试着把客户挪到相邻车成本降了就接受。二是车辆数用满时的兜底处理剩余客户强行并入最后一车会产生超载或超时这部分由目标函数的惩罚项接住保证算法不会因为「装不下」直接崩掉。解码是整条链路里调用最频繁的函数它的效率直接决定程序能跑多大算例。check_route里每次都在重新累加实际实现时可以缓存前缀和这里保持最朴素的写法是为了让逻辑清楚优化放到 4.2 讲。3.2 OX 顺序交叉保住片段、补齐剩余一维排列上的交叉算子首选 OXOrder Crossover。做法是随机切一段区间子代先原样继承父代 A 的这一段剩下的位置按父代 B 的顺序从断点之后依次填入已经在片段里出现过的客户跳过。这样每个客户恰好出现一次不需要任何修复。import random def ox_crossover(p1, p2): n len(p1) a, b sorted(random.sample(range(n), 2)) child [None] * n child[a:b 1] p1[a:b 1] # 继承父代 A 的片段 fill [c for c in p2 if c not in child] # 父代 B 中未被继承的客户保持相对顺序 k 0 for i in range(n): if child[i] is None: child[i] fill[k] k 1 return child参数上只有切点位置a、b是随机的其余都是确定性操作所以这个算子不会引入非法解也不需要额外的可行性修复。fill那一步之所以要按父代 B 的原始顺序取是因为顺序本身携带了「先服务谁后服务谁」的信息打乱就等于把父代 B 的基因全丢了。实践中 OX 单用效果一般原因是它倾向于保留长片段种群多样性掉得快。常见补法是交叉之后加一步「随机重排一小段」或者用两种交叉算子交替——一半子代走 OX另一半走基于位置的交叉让不同来源的结构都有机会传下去。3.3 变异算子与自适应概率变异的作用是把搜索从局部拉出来。VRPTW 上有效的主要是三种交换两个客户的位置、把一段区间整体逆序、把单个客户拎出来插到别处。三种的作用范围不同交换扰动小、适合精细调整逆序一次改动一大片、适合跳出局部插入介于两者之间。def mutate(chrom, pm): 按概率 pm 执行一次变异返回新染色体。 if random.random() pm: return chrom[:] child chrom[:] op random.choice([swap, reverse, insert]) i, j sorted(random.sample(range(len(child)), 2)) if op swap: child[i], child[j] child[j], child[i] elif op reverse: child[i:j 1] reversed(child[i:j 1]) else: node child.pop(j) child.insert(i, node) return child变异概率pm固定成 0.05 或 0.1 是最省事的做法但在实际运行里你会发现前期种群差异大、需要的是保留而不是破坏后期个体趋同、需要加大扰动。所以pm一般做成随迭代衰减再回升的曲线或者干脆根据种群多样性自适应连续若干代最优解没变化就把pm抬高 50%直到出现改进再降回来。提示三种变异算子的比例不要平均分。我一般设成交换 50%、插入 35%、逆序 15%逆序太频繁会把已经收敛好的局部结构反复打碎。3.4 锦标赛选择与精英池选择用锦标赛最简单也最稳每次随机抽 k 个个体取成本最低的那个进下一代。k 取 2 到 4k 越大选择压力越强、收敛越快但也越容易早熟。精英池单独维护一份每代把全局最好的若干个个体原样复制到下一代不做交叉不做变异作用是保证最优解不会因为一次糟糕的交叉被丢掉。精英池大小取种群规模的 5% 到 10% 就够取太多会导致种群被少数几个个体垄断多样性断崖式下跌。有个细节精英个体进入下一代之后最好把它从锦标赛的候选池里剔除否则它会被反复选中几代之内就把整个种群填满同一个基因型。这个问题在 5.1 会展开讲。4. 求解流程落地主循环、缓存与结果输出算子和数据结构都齐了剩下的就是把它们串成一个能跑、能停、能出结果的循环。这部分代码本身不难难的是几个工程问题跑多久算收敛、缓存加在哪一层、结果怎么落盘才能拿去写报告。这一章按主循环、缓存、输出、参数标定四块讲每块都给可运行的骨架。4.1 主循环骨架与收敛判定主循环做四件事评估当前种群、产生下一代、更新精英池、记录收敛曲线。终止条件一般给两个一个是最大迭代代数一个是连续若干代最优解改进幅度小于阈值两个谁先满足就停。def solve(inst, dist, pop_size100, gens500, pc0.9, pm0.1, patience60): pop init_population(inst, pop_size) # 见 3.1随机排列即可 best, best_cost None, float(inf) history, stall [], 0 for g in range(gens): scored [(objective(ind, inst, dist)[0], ind) for ind in pop] scored.sort(keylambda x: x[0]) if scored[0][0] best_cost - 1e-6: best_cost, best scored[0] stall 0 else: stall 1 history.append(best_cost) if stall patience: # 连续多代无改进提前停 break elites [ind for _, ind in scored[:max(1, pop_size // 10)]] new_pop elites[:] while len(new_pop) pop_size: p1 tournament(scored, k3) p2 tournament(scored, k3) child ox_crossover(p1, p2) if random.random() pc else p1[:] new_pop.append(mutate(child, pm)) pop new_pop return best, best_cost, historypatience这个参数值得单独调。设得太小三十代没改进就停往往停在某个还不错的局部解上设得太大后期每代改进微乎其微白白烧时间。我一般按种群规模取patience pop_size * 0.6左右一百个个体的种群对应六十代跑一轮几分钟能出结果。history一定要留着答辩和报告里那张收敛曲线就是从这里出的。曲线长什么样直接反映了参数调得好不好一路平滑下降说明选择压力合适前二十代断崖式下跌然后长期走平基本可以判定早熟。4.2 距离矩阵与适应度缓存check_route每次调用都要算dist[prev][cid]如果距离是现算的欧氏距离一次sqrt就是一次开销几十万次调用下来非常可观。做法很直接初始化时一次性把 n×n 矩阵算好之后全部查表。import math def build_dist(inst): 预计算距离矩阵行列为 0..n0 是仓库。 pts [inst.depot] inst.customers n len(pts) d [[0.0] * n for _ in range(n)] for i in range(n): for j in range(i 1, n): v math.hypot(pts[i].x - pts[j].x, pts[i].y - pts[j].y) d[i][j] d[j][i] v return d一百个客户的算例矩阵是一万个数占内存可以忽略但省下来的重复计算很实在。再往上一层还能加适应度缓存同一代里如果有两条染色体解码出来的路线完全一样就没必要算两遍。用路线的元组做 key 存一个字典命中率在收敛后期能到 20% 以上。缓存要按代清空不然内存会一直涨这也是个常见的翻车点。如果算例规模上到几百个客户单进程跑不动的话评估这一步是可以并行化的把种群切片丢进进程池每个进程独立算适应度回来再合并。注意random模块在多进程下的种子要分别设否则所有进程产生同一批随机数等于没并行。4.3 结果落盘CSV 明细与路线图算法跑完只打印一个总成本是不够的验收的时候要能看到每辆车跑了哪些点、每个点几点到达、有没有等待。所以输出分两份一份是汇总 CSV一行一条路线一份是明细 CSV一行一个客户。import csv def export(result, inst, dist, prefixout): best, cost, history result routes decode(best, inst, dist) with open(f{prefix}_routes.csv, w, newline, encodingutf-8) as f: w csv.writer(f) w.writerow([route_id, stops, distance, load, return_time]) for i, r in enumerate(routes, 1): ok, d, over, late, back check_route(r, inst, dist) load sum(inst.customers[c].demand for c in r) w.writerow([i, 0- -.join(map(str, r)) -0, round(d, 2), load, round(back, 2)]) with open(f{prefix}_history.csv, w, newline, encodingutf-8) as f: w csv.writer(f) w.writerow([generation, best_cost]) w.writerows(enumerate(history))stops那一列写成0-3-7-12-0的形式是为了让人一眼看出车辆从仓库出发、经过哪几个客户、回到仓库不用再去翻客户表。return_time单独列出来验收时用它检查司机有没有超工时。路线图用 matplotlib 画散点连线就行仓库画成方块客户画成圆点不同车辆用不同线型区分图例标上车号。图形的价值在于快速发现问题如果某条线在地图上绕出一个大三角基本可以确定是切分点选得不好。4.4 参数标定别拍脑袋定种群规模种群规模、交叉概率、变异概率、迭代代数这四个参数互相影响单因素调法很容易得出错误结论。可行的做法是小规模网格搜索种群规模取 50、100、150 三档变异概率取 0.05、0.1、0.2 三档其余固定每个组合跑五次取平均看哪个组合的平均成本和方差都低。参数候选值观察重点种群规模50 / 100 / 150小于 50 时多样性不足大于 150 收益递减交叉概率0.7 / 0.9低于 0.7 时收敛明显变慢变异概率0.05 / 0.1 / 0.20.2 前期探索好、后期破坏太大迭代代数300 / 500 / 800配合 patience 使用别单看总代数跑五次取平均这一步不能省。遗传算法本身是随机算法单次结果波动可能有 5% 到 10%只看一次很容易把运气当成参数好。方差同样重要一个平均成本低但波动大的参数组合在实际交付里是不能用的——今天跑出 830明天跑出 950没法跟人解释。5. 常见问题排查五类最容易翻车的现象与处理算法跑起来了不代表跑对了。这一章列的五个现象是我在不同项目里反复见到的按出现频率从高到低排。每一条都按「看到什么、为什么、怎么办」写对着排查基本能定位到问题。5.1 种群十几代就长得一模一样现象收敛曲线前十五代下降很快之后完全走平把最终种群打印出来一看一百个个体里有八十多个成本完全一样路线也几乎重合。原因选择压力过大加上精英复制过多。锦标赛的 k 取到 5 以上或者精英池占了种群的 20%少数几个好个体会在几代之内占领整个种群交叉两个几乎相同的父代产生不了新结构变异概率又低等于停止了搜索。解决把锦标赛 k 降到 2 到 3精英池压到 5%同时在每代结束前检查一次种群的唯一路线数低于种群规模的一半就往里注入一批随机生成的个体。注入比例取 10% 左右太多会破坏已经积累的结构。另一个办法是让变异概率随停滞代数递增连续二十代没改进就把pm从 0.1 抬到 0.2出现改进再降回去。5.2 跑完一看一半车辆超载现象总成本看着挺漂亮导出 CSV 逐条核对发现好几条路线的装载量超过了车辆容量时间窗也有一堆晚到。原因罚系数给得太小算法算过账——超载一点点能省下可观的行驶距离罚的那点成本完全抵消得了。这在搜索空间大的时候特别容易发生因为前期可行解稀少稍有违约的个体反而更容易存活。解决先查解码逻辑确认check_route里的over和late确实被累加进了目标函数而不是算完就丢。确认无误后把两个罚系数抬到距离项的 500 倍以上再看可行解占比。还有一个更彻底的做法在解码之后加一道强制修复发现超载就把该车最后一个客户挪到下一车反复直到所有车辆都不超载修不好的才交给惩罚项。这样能保证最终输出的解一定可执行。5.3 同样的参数两次结果差 15%现象参数一个字没改今天跑出总距离 826明天跑出 954跟人解释不清楚哪个才是真实水平。原因随机种子没有固定。遗传算法从初始化到交叉变异全靠随机数种子不同搜索轨迹完全不同结果自然有差异。解决把种子作为参数暴露出来实验阶段固定种子保证可复现正式评估阶段换十个不同的种子各跑一次报平均值和标准差。这一步在答辩里特别有用被问到「你这个结果稳不稳定」的时候能直接拿出十次运行的分布比嘴上说「挺稳的」有说服力得多。import random def set_seed(seed): random.seed(seed) # 如果用了 numpy 的随机数这里要一起设 try: import numpy as np np.random.seed(seed) except ImportError: pass5.4 客户数从 25 加到 100程序卡死现象小算例几秒钟出结果换成一百个客户的算例跑了一晚上还没停内存占用一路上涨。原因两个地方最可疑。一是距离矩阵没有预计算每次评估都在现算hypot调用次数随客户数平方增长。二是适应度缓存没有按代清理字典越存越大最后内存被吃光开始换页速度断崖式下跌。解决距离矩阵在初始化阶段一次算完之后只查表。缓存按代清空或者用固定大小的 LRU 字典。如果还慢先做一次 profile看时间到底花在check_route还是decode上——多数情况是check_route被调用次数过多可以对decode加一层「前缀可行性」缓存把已经确认可行的一段路线结果存下来复用。5.5 明明没超时程序却报大量违约现象手工核对路线每个客户的到达时间都在时间窗内程序却报告说有大把超时总成本虚高。原因单位不统一。距离算的是公里时间窗写的是分钟speed又填成 1等于把公里当分钟用所有到达时间都被放大了六十倍。或者算例文件里时间窗是相对仓库出发时刻的偏移量读进来当成绝对时刻用整体错位。解决在Instance里把单位写进注释speed的语义固定为「单位时间行驶的距离」读算例之后先打印前三个客户的坐标、需求、时间窗人工核对一遍量级。这个检查花三十秒能省掉半天的排查。这类问题最阴的地方在于程序不报错只是结果不对属于典型的静默失败。6. 验收与答辩用三组对照实验证明这套解站得住方案交上去评审最想问的其实就一句你这个结果凭什么说好。光给一个总成本数字是答不了的因为没有参照系。可行的办法是做三组对照随机解、最近邻贪心解、遗传算法解同一份算例、同一个目标函数把三组结果摆在一张表里。方案总距离车辆数超载总量超时总量可行随机排列2140.6612.4380.2否最近邻贪心1183.240.0145.7否遗传算法本文方案892.530.00.0是这张表的说服力在于它展示了三件事遗传算法在距离上比贪心好一截在车辆数上更省最重要的是它把两个违约项压到了零。贪心解距离不算离谱但时间窗违反了一百多分钟实际执行时客户投诉是躲不掉的。评审看到这一列 0比看到距离从 1183 降到 892 更买账。配套再准备两张图。一张是收敛曲线横轴迭代代数、纵轴当前最优成本曲线应该呈现前期陡降、中期缓慢改进、后期走平的三段形态。如果曲线是一路直线下降到最后一代说明迭代代数还不够或者种群太小。另一张是路线图三条线从仓库散开尽量不交叉能直观看出每条车负责哪一片区域。答辩环节高频的三个问题提前把答案备好。第一个是「为什么用遗传算法而不是精确算法」答案是精确算法在客户数上到几十之后时间不可控遗传算法能在可接受时间内给出高质量可行解并且约束项扩展方便。第二个是「参数怎么定的」直接拿出 4.4 的网格搜索表说明每个组合跑了五次取平均不是拍脑袋。第三个是「结果稳不稳定」拿出十次不同种子的运行分布报均值和标准差同时说明方差主要来自哪里。还有个容易被忽略的验收项解的可行性要能被独立验证。写一个脚本读入输出的 CSV重新走一遍 2.2 的检查逻辑逐条打印「超载 0、超时 0、车辆数 3」把这段输出贴进报告附录。这一步的意义在于证明结果不是程序自己说自己好而是换一套代码重新算过还是好。从那以后我每次交这类方案都会强制走一遍「随机解、贪心解、遗传算法解」三段对照把三份结果摆在同一个表里再谈优化了多少。没有这个基线任何百分比都是空的。希望帮到你。本文还有配套的精品资源点击获取