简介:这套外卖配送遗传算法优化MATLAB代码面向物流配送方向的学习者与研究人员,聚焦配送路径规划、订单与骑手匹配及等待时间控制等实际问题,适合具备MATLAB基础、希望用智能优化算法完成课程设计或项目复现的中级读者。压缩包共4个文件,约191KB,包含3个m脚本与1份pptx演示文稿:脚本覆盖算法主流程、配送匹配和等待时间计算,演示文稿则梳理问题背景、简化模型、算法原理与实验结果。已有3614人学习,说明该主题在配送优化学习中关注度较高。读者可据此理解遗传算法中种群初始化、适应度评估、选择、交叉与变异的具体落地方式,也能参考匹配策略与等待时间指标的设计思路,并借助演示内容对照实验分析,为外卖配送路径优化及类似调度问题提供可复用的代码框架和建模参考。
1. 从一张跑不顺的配送单说起:外卖配送遗传算法优化的切入点
外卖配送路径优化真正麻烦的地方在于:当订单时间窗、骑手载重和并单规则同时压上来之后,最短路线往往反而变成最差解。很多同学拿最近邻贪心跑出的结果去答辩,被问一句“超时订单如何处理”就容易答不上来。这套用遗传算法做外卖配送优化的 MATLAB 代码面向的正是这类场景:十几到上百个订单、若干骑手、带时间窗的取送混合问题。它适合做物流与运筹方向毕设、课程设计的同学,也适合要给调度系统做离线策略验证的从业者。以下按“建模、算法设计、工程实现、排查、验收”的顺序拆开讲,拿到压缩包后可以对着改参数。
2. 问题建模:把配送单抽象成带时间窗的多车型路径问题
很多人一上手就写遗传算法主循环,结果写到一半发现“适应度函数不知道算什么”。问题出在建模这一层没定下来。配送调度里真正要固化的只有三件事:决策变量是什么、目标函数怎么合并、约束在代码里体现为哪个位置判断。这三件事定完了,遗传算法只是一个搜索器。
2.1 决策变量与加权目标函数
决策变量不是“每辆车的路线”,而是两层:第一层,哪些订单交给同一位骑手;第二层,同一位骑手按什么顺序跑。如果直接把这两层写进染色体,搜索空间会大到几乎搜不动,而且大量个体是违法解。更常见的做法是把两层压成一层:染色体只表示一个“大路径”(所有订单的访问次序),解码时按载重和时间窗贪心切分成若干骑手段。这样染色体天然是 1 到 N 的排列,交叉变异都好写。
目标函数通常是四个量打架:总里程、超时单数、启用骑手数、平均送达时长。四个都做成向量去求帕累托前沿,答辩时解释成本太高;工程上更实用的是加权成一个标量:
F = w1 * 总里程(km) + w2 * 超时总分钟(min) + w3 * 骑手数权重的取法有讲究。w1 = 1作基准,w2表示“晚 1 分钟等价于多跑几公里”。城区配送我一般取w2 = 0.3 ~ 0.5,也就是晚一分钟抵三百到五百米;如果业务方对超时零容忍,w2提到 2 以上,算法会宁可绕远也不超时。w3表示多开一位骑手的代价,通常按“该骑手一趟的固定成本折算成里程”,取 3 到 8 之间比较合理。这三个数不是拍脑袋定一次就完事,第 6 章会讲怎么做敏感性分析。
2.2 约束条件在代码里的落点
约束不能只写在文档里,必须落到具体某一行判断上,否则调试时根本不知道是算法不行还是约束写错。这套代码涉及的约束和落点如下:
| 约束 | 业务含义 | 代码落点 |
|---|---|---|
| 载重上限 | 电动车后箱一次最多装 6 格 | 解码函数切分时判断load + demand <= cap |
| 硬时间窗 | 用户期望送达时间区间 | 解码时判断arrive <= tw(node,2),超了必须新开路径 |
| 早到等待 | 早到不能算违约,只能等 | 到达时间取max(arrive, tw(node,1)) |
| 取送配对 | 同一订单先取餐再送餐 | 解码后校验取餐点在送餐点之前,否则整条染色体加惩罚 |
| 单趟最大单量 | 防止一条路径挂 20 单 | 与载重合并成一个cap参数 |
有一点容易被忽略:硬时间窗在解码阶段就用“超了换人”的方式处理,会让算法过度保守,稍微堵一点就没有可行解。更稳的做法是解码时允许超时,超时量转成惩罚计入适应度,让搜索自己去平衡。这也是为什么第 2.1 节的目标函数里有一个超时项——它既是优化目标,也是软化的约束。
2.3 数据表结构与距离矩阵的预处理
数据输入建议固定成三张表,避免每次改数据都要动代码。订单表至少包含:订单号、取餐点坐标、送达点坐标、最早可取时间、最晚送达时间、占用格数、取餐服务时长、送达服务时长。骑手表包含:骑手编号、起点坐标、载重上限、单车成本。再加一张节点坐标表,把取餐点和送达点都摊平成节点编号,距离矩阵才好建。
距离不能直接用欧氏距离。城区道路有绕行系数,我一般把直线距离乘 1.25 到 1.4,再按平均车速折算时间:
% coord: 第 1 列 x,第 2 列 y,第 1 行为配送站,单位 km n = size(coord, 1); D = zeros(n); for i = 1:n for j = 1:n D(i,j) = norm(coord(i,:) - coord(j,:)) * 1.3; % 1.3 为道路绕行系数 end end T = D / 25 * 60; % 平均车速 25 km/h,折算成分钟D用于算里程和成本,T用于算到达时刻,两者分开存是有必要的——里程影响的是油费与考核,时间影响的是超时判定。绕行系数和平均车速这两个参数要根据实际城市改,取小了算法会过于乐观地并单,最终算出来的方案实际上跑不完。
3. 遗传算法主体:编码、交叉、变异与适应度的四件套
遗传算法本身不复杂,难的是每个算子都要和“路径问题”这个结构对齐。纯排列编码套标准交叉,很容易生出重复基因或断掉的路径。下面按执行顺序把这四件套拆开。
3.1 染色体编码:大路径加贪心解码
染色体是1×N的整数排列,基因值是订单节点编号,含义是“访问次序的优先级”。解码函数从左到右扫,能塞进当前骑手就塞,塞不进就换下一位骑手:
function routes = decode(chrom, cap, demand, T, tw) % chrom : 1×N 排列,基因是节点编号 % cap : 单个骑手载重上限(格) % demand : 各节点占用格数 % T : 时间矩阵;tw : 时间窗,tw(i,1) 最早,tw(i,2) 最晚 routes = {}; cur = []; load = 0; prev = 1; t = 0; for k = 1:numel(chrom) node = chrom(k); t = t + T(prev, node); t = max(t, tw(node,1)); % 早到等待,不算违约 if load + demand(node) <= cap && t <= tw(node,2) cur(end+1) = node; load = load + demand(node); prev = node; else routes{end+1} = cur; % 装不下就换下一位骑手 cur = node; load = demand(node); prev = 1; % 新骑手从配送站出发 t = max(T(1,node), tw(node,1)); end end if ~isempty(cur), routes{end+1} = cur; end end解码的副作用是“染色体的适应度不由基因唯一决定”,还受切分策略影响,因此同一条染色体多次评估必须给出一致结果——第 5 章会讲这个坑。这个解码的好处是任何排列都能解码出一个合法方案,不会出现“交叉完个体全废”的情况,搜索效率比带修复算子的编码高不少。
3.2 交叉算子:OX 顺序交叉保住路径片段
路径类问题最常用的是 OX(Order Crossover):随机切两刀,把父代 A 的中间段原样保留,剩下的位置按父代 B 的出现顺序补齐。这样既保留了 A 的一段连续访问次序(这段次序往往对应一段合理的连续配送),又保证子代仍然是合法排列。
function child = ox(p1, p2) n = numel(p1); cut = sort(randperm(n-1, 2)); child = zeros(1, n); child(cut(1):cut(2)) = p1(cut(1):cut(2)); % 保留父代 A 的片段 rest = p2(~ismember(p2, child(cut(1):cut(2)))); % 父代 B 去掉已用基因 idx = [cut(2)+1:n, 1:cut(1)-1]; % 环形填回剩余位置 child(idx) = rest; endcut用randperm取两个不同切点再排序,保证切点不重合。rest长度一定等于n - (cut(2)-cut(1)+1),和idx长度一致,不会出现维度不匹配的报错。交叉概率pc取 0.8 到 0.9 之间比较常见,取太高会破坏优良个体,取太低则种群更新慢。
3.3 变异与局部搜索:2-opt 是提升解质量的关键
只用交换变异(随机两点互换)跑出来的解往往“大方向对、局部打结”,路线上有明显交叉。经验做法是变异之后再加一次轻量局部搜索。用逆序变异(随机选一段反转)做粗调,用 2-opt 做精调:
function route = twoOpt(route, D) % 对单条路径做一次完整的 2-opt 扫描,只接受能降里程的反转 improved = true; while improved improved = false; for i = 1:numel(route)-2 for j = i+2:numel(route) a = route(i); b = route(i+1); c = route(j); if j+1 <= numel(route), d = route(j+1); else, d = 1; end % 末尾回到配送站 delta = D(a,c) + D(b,d) - D(a,b) - D(c,d); if delta < -1e-9 route(i+1:j) = route(j:-1:i+1); % 反转中间段 improved = true; end end end end end2-opt 的计算量是 O(L²) 每轮,对 30 个点以内的单条路径完全够用,超过 80 个点要考虑加邻域限制(只对空间上相近的点做 2-opt),否则一代跑完就要几十秒。变异概率pm我一般给 0.05 到 0.15,2-opt 不要每代对全部个体做,只对精英个体和前 30% 做,否则运行时间会失控。
3.4 适应度函数与惩罚系数的量级校准
适应度把所有项折算成同一个量纲,代码里就这么几行:
function fit = fitness(chrom, P) routes = decode(chrom, P.cap, P.demand, P.T, P.tw); totalD = 0; late = 0; for r = 1:numel(routes) rt = [1, routes{r}, 1]; % 配送站出发、返回 for k = 1:numel(rt)-1 totalD = totalD + P.D(rt(k), rt(k+1)); end late = late + lateness(routes{r}, P); % 累计超时分钟 end fit = P.w1*totalD + P.w2*late + P.w3*numel(routes); end惩罚系数的校准有一套土办法:先让w2 = 0跑一次,看最优解的超时总量是多少分钟;再让w2从小到大扫,找到“超时量明显下降、总里程没有暴涨”的拐点。这个拐点通常出现在 0.2 到 0.6 之间。全部参数集中在一个config()函数里,答辩时改参数只要改一处:
| 参数 | 建议取值 | 说明 |
|---|---|---|
popSize | 80 ~ 150 | 订单数 50 以上取上限 |
maxGen | 300 ~ 800 | 配合收敛曲线判断,不是越大越好 |
pc | 0.85 | 顺序交叉概率 |
pm | 0.10 | 逆序变异概率 |
eliteN | 2 ~ 5 | 精英保留数,防止最优解被破坏 |
w2 | 0.3 ~ 0.5 | 超时惩罚,按业务容忍度调 |
w3 | 3 ~ 8 | 骑手启用成本折算 |
4. MATLAB 工程实现:从读数据到出图的一条完整链路
算法写得再漂亮,如果主脚本跑起来要手改三处路径、五个变量,别人拿过去也用不了。这一章讲工程层面的组织方式:入口、配置、可视化、基线对比,四块。
4.1 主流程骨架与随机种子
主脚本只做流程串联,不写业务细节。开头固定随机种子,保证同一个种子跑出同一份结果——这一点在答辩现场很关键,评委让你重跑一次,结果必须一模一样。
rng(20240501); % 固定种子,结果可复现 P = config(); % 所有可调参数集中在这里 pop = initPop(P); % 随机生成初始种群 bestFit = zeros(1, P.maxGen); for g = 1:P.maxGen fit = arrayfun(@(i) fitness(pop(i,:), P), 1:P.popSize); newPop = select(pop, fit, P); % 锦标赛选择 newPop = crossover(newPop, P); % OX 交叉 newPop = mutate(newPop, P); % 逆序变异 newPop = localSearch(newPop, P); % 对前 30% 做 2-opt pop = [elite(pop, fit, P.eliteN); newPop(1:P.popSize-P.eliteN, :)]; bestFit(g) = min(fit); endarrayfun那行是逐个体算适应度,写法简洁但在大种群下略慢,想提速可以改成先向量化解码统计再统一计算。elite取当前代最优的若干个体直接进下一代,是防止解质量回退最省事的手段。
4.2 配置分离与结果落盘
config()返回一个结构体,把数据路径、权值、算子参数、随机种子全部收进去。这样做的好处是第 6 章的对照实验只需要改结构体字段,主脚本一行不动。结果建议直接存.mat,包含最优染色体、解码后的路线、收敛曲线和运行耗时,方便后面写论文时反复取用,不用每次重跑。
数据读取部分建议做一层容错:坐标表里如果有 NaN,距离矩阵会整片变 NaN,适应度全是 NaN,min返回空,报错信息会指向一个跟真实原因八竿子打不着的地方。这是典型的玄学报错,第 5 章展开。
4.3 三张图定生死:路线图、收敛曲线、负载分布
答辩 PPT 上真正有说服力的图就三张。路线图按骑手编号上不同颜色,标出配送站和超时订单;收敛曲线画每代最优值和种群均值两条线,两条线的间距能说明种群多样性;负载分布画每个骑手的单量、里程、超时分钟三根柱。第三张图经常被忽略,但它最能回答“方案公不公平”这类追问。
figure; hold on; cmap = lines(numel(routes)); for r = 1:numel(routes) rt = [1, routes{r}, 1]; plot(coord(rt,1), coord(rt,2), '-o', 'Color', cmap(r,:)); end plot(coord(1,1), coord(1,2), 'kp', 'MarkerSize', 12, 'MarkerFaceColor', 'k'); title(sprintf('总里程 %.1f km,超时 %d 单', totalD, nLate));lines最多给 7 种区分度较好的颜色,骑手数超过 7 位时要换用hsv或者手工指定配色,否则两条线颜色接近,图上根本分不清谁是谁。
4.4 与贪心和随机基线做对比
只给一个“总里程 42.7 km”没人知道好不好。必须有三组对照:随机排列解码、最近邻贪心、遗传算法。三组跑同一份数据、同一个种子,把总里程、超时单数、启用骑手数、耗时列成一张表。通常遗传算法相对最近邻能把总里程压下来 8% 到 15%,把超时单数压下来一半以上——这两个数字就是方案的价值所在。
5. 避坑与排查:解质量上不去时按这个顺序查
遗传算法最难受的地方是“不报错,但结果就是不对”。下面五条是我踩过的顺序,从现象倒推原因,基本能覆盖八成问题。
5.1 收敛曲线前 20 代就压平了
现象:bestFit迅速下降后一条直线,种群均值也跟着贴上去,后续 300 代毫无变化。原因通常是种群早熟——初始种群随机性不足,或者精英比例过大,几代之后全种群都是同一个解的变体。解决:先检查initPop是不是用了固定排列,再检查eliteN是否超过 5;把变异概率从 0.05 提到 0.1 以上,交叉概率略降,必要时加一个“随机重启”——连续 30 代最优值不变就重新生成一半个体。
5.2 每代都提示无可行解
现象:解码结果里大量路径为空,或者所有个体的适应度都等于一个巨大的常数值。原因一般是时间窗数据本身不自洽:某订单的最晚送达时间早于最早可取时间加行驶时间,无论怎么排都不可能准时。解决:在数据预处理阶段加一道校验,把“送达窗口起点 < 取餐窗口起点 + 最短行驶时间”的订单筛出来单独打印,人工确认后再进算法。这类脏数据不清理,调参调到天亮也没用。
5.3 换一组订单,结果水平断崖式下跌
现象:A 城市数据跑得很好,换成另一组同样规模的订单,总里程直接翻倍。原因多半是距离矩阵的单位和绕行系数被硬编码了,换数据后坐标系变了(比如从经纬度换成像素坐标),量纲完全错位。解决:把距离计算抽成函数,入口处打印“距离矩阵最大值/最小值/均值”三个数,跑之前肉眼看一眼,正常城区订单均值应该在 2 到 8 km 之间,超出这个范围就是量纲错了。
5.4 订单数一多,运行时间成倍爆炸
现象:30 单跑 10 秒,80 单跑 10 分钟。原因是 2-opt 的 O(L²) 在大路径上失控,加上每代对全部个体做局部搜索,复杂度直接乘起来。解决:局部搜索只作用于精英和前 30% 个体;2-opt 加邻域过滤,只对距离矩阵中前 15 个最近邻点做反转判断;maxGen从 800 降到 300,因为后 500 代的提升通常不到 1%。
5.5 同一个种子跑两次,结果不一样
现象:rng明明设了,重跑一次总里程还是差几十米。原因有三类:循环里用了rand之外的不确定源,比如parfor并行时各 worker 的随机流互相独立;解码函数里有unique或sort改变了同值元素的顺序;2-opt 的接受条件用了浮点严格小于号,边界值在不同机器上判断不同。解决:去掉并行、把比较改成delta < -1e-9这种带容差的写法,然后在主脚本首尾各打印一次rng的当前状态核对。
注意:调试期建议把
maxGen临时调到 20,跑一次只要一两秒,五个坑用不了十分钟就能全排一遍,比在 800 代的循环里等结果快得多。
6. 验收与答辩:用三组对照和一次重跑把方案讲清楚
验收这套东西不看代码写得多花,看三件事:解比基线好多少、好在哪个指标上、换个数据还灵不灵。下面是一张我常用的对照表,同一份数据、同一个种子,跑完直接填:
| 方案 | 总里程(km) | 超时单数 | 启用骑手数 | 求解耗时(s) |
|---|---|---|---|---|
| 随机排列基线 | 68.4 | 11 | 6 | 0.02 |
| 最近邻贪心 | 47.2 | 5 | 5 | 0.01 |
| 遗传算法 | 41.5 | 1 | 5 | 6.8 |
| 遗传算法 + 2-opt | 38.9 | 0 | 5 | 14.2 |
表里最值得讲的是最后一行相对第三行的增量:多花 7 秒换来 2.6 km 和 1 个超时单,这笔账划不划算要结合业务说——如果一单超时罚款折算 1.5 km,那这笔交易是赚的。这种“把算法指标翻译成钱”的说法,比单说“收敛更快”有说服力得多。
答辩现场被追问最多的无非三个点。第一,“为什么用遗传算法不用精确求解”,答案是订单上到 50 单以后,VRPTW 的精确解算到天亮也出不来,启发式是工程上的必然选择,这时候可以顺手给出贪心基线作为对照。第二,“参数怎么定的”,把第 3.4 节那张参数表和权重敏感性分析拿出来,跑过的w2 = 0.1/0.3/0.5/1.0四组结果摆上去,说明拐点在哪。第三,“结果能复现吗”,现场改一次随机种子重跑,收敛曲线形状一致、最终值在同一量级,就够了——评委要的是稳定性,不是完全相同的数字。
从那以后我每次交方案前都强制走一遍流程:换三组数据、固定种子重跑一次、把所有参数写进附录。这三步花不了一小时,但能挡掉答辩现场九成的追问。希望帮到你。
本文还有配套的精品资源,点击获取