尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

基于模拟退火与Dijkstra的外卖配送路径优化建模与Matlab实现

基于模拟退火与Dijkstra的外卖配送路径优化建模与Matlab实现 1. 项目概述从“送餐危机”到数学建模的实战解析外卖骑手的送餐效率问题早已不是简单的“跑得快”就能解决的。尤其是在2021年数维杯数学建模A题“外卖骑手的送餐危机”中这个问题被抽象成了一个典型的运筹学与路径优化难题。题目通常会给出一个包含餐厅、顾客分布、道路网络、时间窗、骑手数量与速度等约束的复杂场景要求参赛者建立数学模型优化配送方案以最小化总配送时间、成本或最大化订单完成率等目标。这不仅仅是数学竞赛更是对现实世界中即时物流核心痛点的深度模拟。对于学习运筹学、物流管理、计算机科学的学生或是任何对算法优化感兴趣的朋友来说这道题都是一个绝佳的练手项目它能让你亲身体验如何将纷繁复杂的现实问题转化为可计算、可优化的数学模型并用代码将其求解。这道题的核心在于“多约束条件下的路径优化”。它不像简单的旅行商问题TSP只关心访问所有点的最短路径而是叠加了时间窗顾客要求送达的时间范围、多骑手、载货量限制、动态路况可能以拥堵系数体现等多重现实约束。解决这类问题单一算法往往力不从心需要组合拳。从网络热词可以看出大家的关注点高度集中在几个核心工具和算法上Matlab作为强大的数值计算与建模平台模拟退火算法SA和Dijkstra算法作为解决路径与优化问题的利器。本文将基于这些核心工具为你彻底拆解这道题的求解全流程从问题分析、模型建立、算法选择与实现到代码调试和论文撰写要点分享我作为多次带队参赛的“老司机”的一线实战经验与避坑指南。2. 核心问题拆解与建模思路面对“外卖骑手的送餐危机”第一步不是急着写代码而是要把题目描述的现实问题精准地翻译成数学语言。这一步走偏了后面所有努力都可能白费。2.1 关键约束与目标识别首先我们需要从题目中提取出所有关键元素。通常这类题目会包含以下信息点节点信息餐厅位置起点/取餐点、顾客位置终点/送餐点。每个顾客点关联一个订单。道路网络以图的形式给出节点代表路口或地点边代表道路边上有权重如距离、通行时间或时间依赖的拥堵系数。骑手信息骑手数量、初始位置、行驶速度、最大载货量能同时携带的订单数。订单信息每个订单的取餐餐厅、送餐顾客、准备就绪时间、顾客期望送达时间窗最早和最晚送达时间。优化目标最常见的是最小化所有订单的总配送时长或总延误时间也可能是最小化骑手总行驶距离、最大化准时送达率等。识别出这些后我们需要定义决策变量。最核心的决策变量通常是为每个骑手规划一条路径该路径决定了其访问节点餐厅和顾客的顺序。同时还需要确定每个订单由哪个骑手在何时取餐和送餐。2.2 数学模型构建从直观描述到数学公式基于以上我们可以构建一个混合整数规划模型。以最小化总配送时间为目标为例模型骨架如下集合定义R: 骑手集合。N: 所有节点集合包括餐厅、顾客、骑手初始位置虚拟点。O: 订单集合。每个订单o关联取餐点p_o和送餐点d_o。A: 道路弧段集合即连接节点的有向边因为单行道可能存在。参数输入数据t_ij: 从节点i到节点j的行驶时间。[e_i, l_i]: 节点i的时间窗对于顾客是送达时间窗对于餐厅可能是取餐就绪时间。Q: 骑手最大载货量。s_i: 在节点i的服务时间如取餐、送餐耗时。M: 一个极大的正数用于线性化逻辑约束。决策变量x_{ij}^r 1如果骑手r从节点i行驶到节点j否则为0。T_i^r: 骑手r到达节点i的时间。L_i^r: 骑手r离开节点i时的负载携带的订单数。目标函数最小化所有骑手完成其最后一个送餐任务的时间之和或最小化最后一个订单的送达时间makespan。Minimize Z max_{r, i in CustomerNodes} T_i^r或Sum_{r} T_{end}^r约束条件流平衡约束每个骑手从虚拟起点出发最终回到虚拟终点或不停留路径中间每个非起终点的节点进入和离开的弧段数量相等。订单完整性约束每个订单必须被一个骑手完成且取餐点必须在送餐点之前被访问。时间窗约束骑手到达节点i的时间T_i必须在时间窗[e_i, l_i]内或允许违约但施加惩罚。载货量约束骑手在任意时刻携带的订单数不能超过Q。取餐点负载1送餐点负载-1。时间连续性约束如果骑手r从i走到j(x_{ij}^r1)那么到达j的时间T_j^r必须大于等于离开i的时间T_i^r s_i t_ij。这个约束通常需要用大M法线性化处理。子回路消除约束防止路径中出现不包含起终点的循环。这是TSP类问题的经典约束常用MTZMiller-Tucker-Zemlin公式或割平面法。建模心得对于数维杯这类竞赛完全精确的MIP模型可能变量和约束太多求解器在有限时间内无法得到可行解。因此我们通常采用“精确模型描述问题 启发式/元启发式算法求解”的策略。即用上述模型清晰地定义问题但在求解时转向模拟退火、遗传算法等更灵活的方法。2.3 算法选型策略为什么是模拟退火Dijkstra从热搜词就能看出模拟退火算法SA和Dijkstra算法是解决本题的黄金组合。为什么Dijkstra算法负责解决底层路径规划问题。在我们的模型中节点间的行驶时间t_ij不是简单的直线距离而是基于道路网络的最短时间路径。题目给出的道路网络图本质上是一个带权图。我们需要频繁计算任意两个节点之间的最短时间。Dijkstra算法正是解决单源最短路径问题的经典算法。在预处理阶段我们可以以每个节点为源点运行一次Dijkstra算法将结果存储在一个距离/时间矩阵中后续优化算法直接查表避免重复计算极大提升效率。模拟退火算法SA负责解决顶层路径优化问题。我们将每个骑手的路径序列即节点访问排列看作一个“解状态”。SA通过模拟固体退火过程以一定概率接受“劣解”从而跳出局部最优向全局最优搜索。它特别适合解决像带时间窗的车辆路径问题VRPTW这种NP-Hard的组合优化问题。我们可以设计诸如“交换两个订单的骑手”、“反转某骑手路径中的一段”、“将某个订单插入到另一个位置”等邻域动作来生成新解。组合方式SA在迭代过程中每次评估一个新解一组路径的“优劣”即目标函数值时需要计算这组路径的实际完成时间。计算过程中就需要用到预先算好的节点间最短时间矩阵由Dijkstra算法得出并结合时间窗、载货量等约束模拟骑手沿路径行驶的过程才能算出准确的总时间。所以Dijkstra是SA的“基础设施”。3. 核心算法实现与Matlab编程细节理论清晰后我们来落地到Matlab代码。我会分模块讲解关键部分的实现。3.1 数据预处理与最短路径矩阵计算首先我们需要将题目给出的道路网络通常是邻接矩阵或边列表读入。假设我们有一个n_node个节点的网络dist_matrix是一个n_node x n_node的矩阵dist_matrix(i,j)表示从i到j的直接距离或时间若两点不直接相连则为Inf。function [shortest_time_mat] precompute_shortest_time(dist_matrix) % dist_matrix: 初始直接距离/时间矩阵不连通为Inf n size(dist_matrix, 1); shortest_time_mat zeros(n, n); for i 1:n % 使用Dijkstra算法计算从节点i到所有节点的最短时间 [dist, ~] dijkstra(dist_matrix, i); shortest_time_mat(i, :) dist; end % 注意如果图是有向的dist_matrix可能不对称此循环计算的是所有点对的最短路径。 % 对于无向图矩阵是对称的可以只算一半。 end % Dijkstra算法实现使用优先队列效率更高这里给出基本实现 function [dist, prev] dijkstra(graph, start) n size(graph, 1); dist inf(1, n); dist(start) 0; visited false(1, n); prev zeros(1, n); for i 1:n % 找到未访问节点中距离最小的 [~, u] min(dist .* ~visited visited * inf); if isinf(dist(u)) break; end visited(u) true; % 更新邻居节点距离 for v 1:n if graph(u, v) inf ~visited(v) alt dist(u) graph(u, v); if alt dist(v) dist(v) alt; prev(v) u; end end end end end实操要点对于节点数量较多如500的情况每次迭代都调用循环版的Dijkstra会非常慢。在Matlab中可以考虑使用graph和shortestpathtree函数需要Matlab R2015b以上。将网络数据转换为稀疏矩阵sparse并使用graphallshortestpaths函数来自Bioinformatics Toolbox但已过时。预先计算并存储所有点对的最短路径矩阵。虽然预处理耗时但SA迭代中数百万次的距离查询将变得极快这是典型的“空间换时间”策略。3.2 解的表达与初始解生成在SA中一个“解”需要完整描述所有骑手的任务分配和顺序。一种高效的表达方式是使用两层编码第一层任务分配。一个长度为总订单数的向量assignmentassignment(k) r表示订单k分配给骑手r。第二层任务顺序。对于每个骑手r用一个有序列表route{r}存储他需要访问的节点ID序列。这个序列需要满足对于每个订单取餐点p必须在送餐点d之前。初始解可以采用简单启发式生成例如最近邻法每个骑手从当前位置开始选择距离最近且满足时间窗和载货量约束的未分配订单先取餐点加入路径更新位置重复直至无法添加更多订单。随机分配排序先将所有订单随机分配给骑手然后在每个骑手内部按照取餐点时间窗的先后进行排序生成一个初始路径可能违反送餐顺序约束需要修复。function [assignment, routes] generate_initial_solution(orders, riders, time_matrix) n_orders length(orders); n_riders length(riders); assignment zeros(1, n_orders); routes cell(1, n_riders); for r 1:n_riders routes{r} [riders(r).start_node]; % 从骑手初始位置开始 end % 简单随机分配 for o 1:n_orders r randi(n_riders); assignment(o) r; % 将订单的取餐点和送餐点追加到该骑手路径末尾需要后续优化顺序 routes{r} [routes{r}, orders(o).pickup_node, orders(o).delivery_node]; end % 对每个骑手的路径进行初步排序修复例如确保取餐在送餐前 for r 1:n_riders routes{r} repair_route(routes{r}, orders, assignment, r); end end3.3 目标函数评估模拟配送过程这是整个算法的核心也是最容易出错的部分。给定一个解即所有骑手的路径routes我们需要计算出总配送时间或总成本。function [total_cost, violation] evaluate_solution(routes, orders, riders, time_matrix, time_windows, capacity) total_cost 0; violation 0; % 记录违反约束的惩罚值 for r 1:length(routes) route routes{r}; rider_speed riders(r).speed; current_time 0; current_load 0; current_pos riders(r).start_node; for i 1:length(route) next_node route(i); % 计算行驶时间 travel_time time_matrix(current_pos, next_node) / rider_speed; arrival_time current_time travel_time; % 检查时间窗 [earliest, latest] get_time_window(next_node, orders); if arrival_time earliest current_time earliest; % 等待 violation violation (earliest - arrival_time) * PENALTY_EARLY; % 早到惩罚可选 elseif arrival_time latest violation violation (arrival_time - latest) * PENALTY_LATE; % 迟到惩罚 current_time arrival_time; % 仍然服务但记录违约 else current_time arrival_time; end % 服务时间取餐或送餐 service_time get_service_time(next_node, orders); current_time current_time service_time; % 更新负载 if is_pickup_node(next_node, orders) current_load current_load 1; if current_load capacity violation violation (current_load - capacity) * PENALTY_CAPACITY; end elseif is_delivery_node(next_node, orders) current_load current_load - 1; end current_pos next_node; end % 骑手r的完成时间可能是current_time总成本可以是最晚完成时间或时间和 total_cost max(total_cost, current_time); % 最小化最大完成时间Makespan % 或者 total_cost total_cost current_time; % 最小化总时间 end total_cost total_cost ALPHA * violation; % 将约束违反作为惩罚项加入目标 end避坑指南目标函数中的惩罚系数PENALTY_EARLY, PENALTY_LATE, PENALTY_CAPACITY, ALPHA设置至关重要。设置太小算法会倾向于生成不可行解设置太大会压制对主要目标时间的优化。我的经验是采用自适应惩罚在SA初期惩罚系数可以设小一些允许探索不可行区域随着温度下降逐渐增大惩罚系数迫使搜索向可行解收敛。3.4 模拟退火算法主框架实现现在我们将所有部分组装到SA的主循环中。function [best_solution, best_cost, cost_history] simulated_annealing_vrptw(orders, riders, params) % 参数初始化 T_init params.T_init; % 初始温度 T_min params.T_min; % 终止温度 alpha params.alpha; % 降温系数 max_iter params.max_iter; % 每个温度下的迭代次数 % 生成初始解 [current_assignment, current_routes] generate_initial_solution(orders, riders, params.time_matrix); [current_cost, ~] evaluate_solution(current_routes, orders, riders, params.time_matrix, params.time_windows, params.capacity); best_solution current_routes; best_cost current_cost; cost_history [current_cost]; T T_init; while T T_min for iter 1:max_iter % 1. 在当前解附近产生新解邻域动作 [new_routes, move_type] generate_neighbor(current_routes, current_assignment, orders, riders); % 2. 评估新解 [new_cost, new_violation] evaluate_solution(new_routes, orders, riders, params.time_matrix, params.time_windows, params.capacity); % 3. 计算成本差 (Delta E) delta_cost new_cost - current_cost; % 4. Metropolis准则决定是否接受新解 if delta_cost 0 || rand() exp(-delta_cost / T) current_routes new_routes; current_cost new_cost; % 更新分配信息如果需要 current_assignment update_assignment(new_routes, orders); % 更新历史最优解 if current_cost best_cost new_violation 0 % 通常要求最优解是可行解 best_solution current_routes; best_cost current_cost; end end end % 记录当前温度下的成本 cost_history(end1) current_cost; % 降温 T alpha * T; % 可以在这里增加自适应策略如根据接受率调整迭代次数 end end % 邻域动作示例交换两个订单的所属骑手 function [new_routes, move_type] generate_neighbor(routes, assignment, orders, riders) new_routes routes; % 深拷贝避免修改原数据 n_orders length(orders); % 随机选择两种不同的邻域动作之一 if rand() 0.5 % 动作1随机选择两个订单交换它们的分配骑手 o1 randi(n_orders); o2 randi(n_orders); while o2 o1 o2 randi(n_orders); end r1 assignment(o1); r2 assignment(o2); if r1 ~ r2 % 从r1路径中移除o1的节点插入到r2路径中需保持顺序 % 从r2路径中移除o2的节点插入到r1路径中 % ... (具体实现涉及路径节点的查找、删除、插入和顺序修复) move_type swap_order_between_riders; else % 如果属于同一骑手则尝试其他动作或重新选择 end else % 动作2随机选择一个骑手在其路径中反转一段连续节点的顺序 r randi(length(routes)); if length(routes{r}) 3 % 路径足够长 i randi(length(routes{r})-2); j i randi(length(routes{r})-i-1); new_routes{r}(i:j) fliplr(new_routes{r}(i:j)); % 需要检查反转后是否破坏了取送餐顺序若破坏则修复或拒绝此移动 move_type reverse_segment; end end % 必须调用修复函数确保新路径满足取送餐顺序约束 new_routes repair_all_routes(new_routes, orders); end4. 算法调试、优化与结果分析实现基本框架只是第一步让算法高效、稳定地找到高质量解需要大量的调试和优化。4.1 参数调优没有银弹只有实验模拟退火算法的性能极度依赖参数设置。以下是一些经验性的起始点和建议初始温度T_init应设置得足够高使得在初始阶段几乎所有的劣解都能被接受接受概率 0.8。可以运行一个预热阶段随机生成大量解计算目标函数值的标准差σ令T_init K * σK可以取 10~100。终止温度T_min通常设置得非常小例如1e-8或1e-10。也可以根据迭代次数或连续若干温度下最优解未改进来终止。降温系数alpha通常在0.85到0.99之间。值越大降温越慢搜索越充分但耗时越长。对于复杂问题建议使用0.95或更高。马尔可夫链长度max_iter每个温度下的迭代次数。通常与问题规模相关可以是节点数量的若干倍如100*n。也可以采用自适应长度当接受率低时增加迭代次数。邻域动作设计这是算法的灵魂。除了交换订单、反转片段还可以尝试插入将一个订单从当前路径中移除插入到另一个骑手路径的某个位置。2-opt在单条路径内选择两条边断开并重新连接是优化TSP的经典局部搜索。交叉借鉴遗传算法交换两个骑手路径中的一段。混合策略在SA的每次迭代中以不同概率选择多种邻域动作比单一动作搜索能力更强。调试技巧绘制收敛曲线图迭代次数 vs 当前解成本/最优解成本和温度下降曲线。观察算法是否在初期有充分的“抖动”接受劣解后期是否平稳收敛。如果曲线下降过快可能是初始温度太低或降温太快如果曲线一直平缓可能是邻域动作设计不佳无法有效改进解。4.2 可行性修复与约束处理技巧带时间窗和载货量约束的路径问题随机生成的邻域解很容易违反约束。除了在目标函数中加入惩罚项一个更高效的方法是设计可行性保持或易于修复的邻域动作并配备快速的修复启发式。路径修复启发式当新解因交换、插入等操作导致取餐点位于送餐点之后时需要修复。一个简单的方法是遍历路径每当遇到一个送餐点检查其对应的取餐点是否已出现在它之前。如果没有则向前搜索找到该取餐点并移动到送餐点之前如果移动后不违反其他订单顺序。这个过程可以递归进行。时间窗插入启发式当决定将一个订单插入到某骑手路径的某个位置时可以快速估算插入后对路径时间的影响以及是否会导致后续节点时间窗违约。这比生成完整路径再评估要快得多可以用于在邻域动作中快速筛选有潜力的插入位置。function is_feasible fast_feasibility_check(route, insert_pos, node_to_insert, orders, current_time_vec, time_matrix, time_windows) % 快速检查将节点node_to_insert插入route的insert_pos位置后时间窗可行性 % 这是一个简化检查可能不精确但用于快速过滤 % 假设已知插入前路径各节点的到达时间current_time_vec % 计算插入节点对其自身及后续节点时间的影响... % 如果估算出的新时间窗违反不严重返回true end4.3 结果可视化与论文撰写要点得到优化后的路径方案后可视化是呈现结果最直观的方式。路径可视化在Matlab中可以使用plot、graphplot或geoplot如果有点的经纬度函数。为每个骑手分配一种颜色绘制其行驶路径用不同标记表示餐厅和顾客点。甘特图展示每个骑手的时间线清晰显示何时在何地取餐、送餐以及等待时间、行驶时间。可以使用patch函数手动绘制或搜索Matlab的甘特图脚本。收敛过程图展示SA迭代过程中最优解和当前解的变化趋势。论文撰写核心问题重述与分析用自己的话精炼概括问题并分析其属于哪类经典问题VRPTW, PDPTW等。模型假设与符号说明明确列出你的合理假设如骑手速度恒定、忽略红绿灯并给出完整的符号定义表。模型建立清晰地展示你的数学模型包括目标函数和所有约束条件。即使最终用启发式算法求解一个严谨的数学模型也能体现你的建模思想。算法设计详细说明你为何选择SADijkstra描述解的表达、邻域动作、目标函数计算流程、降温策略等。流程图是加分项。数值实验参数设置列出你所有算法参数的取值并简要说明理由如通过预实验确定。结果展示用表格呈现不同算法或参数下的结果对比总成本、运行时间、骑手利用率等。用图表可视化最终配送方案。灵敏度分析改变某个关键参数如骑手数量、时间窗宽度、订单密度观察目标函数的变化并分析原因。这能极大提升论文深度。模型评价与推广客观评价你模型的优点如求解效率高、方案可行和缺点如对初始解敏感、可能非全局最优。提出可能的改进方向如结合遗传算法、使用更精细的邻域搜索。5. 常见问题排查与进阶优化方向在实际编程和调试中你肯定会遇到各种问题。这里记录一些典型“坑点”和解决思路。5.1 算法运行速度太慢瓶颈分析使用Matlab的profile工具profile on/profile viewer找出最耗时的函数。八成是evaluate_solution或距离查询。优化策略距离矩阵预计算如前所述务必预计算所有点对最短路径并存储为矩阵评估时直接查表O(1)。向量化评估尽量避免在evaluate_solution中使用循环。可以考虑将一条路径的行驶时间计算向量化但鉴于时间窗和负载的逻辑判断完全向量化较难可部分优化。增量评估SA的邻域动作通常只改变解的局部。计算新解成本时可以只计算受影响骑手路径的变化部分而不是重新评估所有路径。这是最大程度的优化但实现复杂。使用编译语言将最核心的evaluate_solution函数用C/C编写通过MEX接口在Matlab中调用可提速数十倍。5.2 算法陷入局部最优解的质量不高提高初始温度增加初始接受劣解的概率。减慢降温速度增大alpha如0.99或在每个温度下进行更多迭代max_iter。丰富邻域动作设计更多样化的移动方式增加搜索空间。重启策略当连续多个温度最优解未更新时保留历史最优解从该解或一个新的随机解开始重新升温进行搜索。混合算法将SA与局部搜索如2-opt, 3-opt结合。在SA接受一个新解后立即对其执行一轮贪婪的局部搜索将其推到局部最优点再进行退火。这种“模拟退火局部搜索”的框架非常有效。5.3 结果波动大不稳定随机种子记录并固定随机数种子rng(seed)便于复现结果和调试。多次运行由于SA是随机算法对同一问题独立运行多次取最好结果作为最终方案并报告中位数、平均值等统计信息。自适应参数实现自适应的降温进度或马尔可夫链长度根据接受率动态调整使算法在不同问题实例上表现更稳健。5.4 进阶优化方向如果你想挑战更高难度可以考虑以下方向考虑动态交通将行驶时间t_ij建模为随时间变化的函数这会使问题变为动态车辆路径问题DVRP评估函数需要实时计算最短路径Dijkstra需升级为时间依赖的最短路径算法如时间依赖的Dijkstra。多目标优化同时优化配送总时间、总距离、骑手工作量均衡度等多个目标。可以使用帕累托优化的思想或通过加权求和将多目标转化为单目标。集成机器学习使用历史数据训练模型预测订单的“紧急程度”或“配送难度”在优化时给予不同权重实现智能调度。并行计算SA的每次迭代是独立的可以并行评估多个邻域解。利用Matlab的parfor进行并行循环能显著缩短运行时间。最后记住数学建模竞赛不仅是比算法更是比解决问题的完整流程从审题、假设、建模、求解、验证到呈现。把每个环节都想清楚、做扎实你的论文和程序自然就能脱颖而出。这道“外卖骑手的送餐危机”题就像一座金矿挖得越深收获的不仅仅是奖项更是解决复杂现实问题的硬核能力。
返回列表