基于图论与几何优化的机器人最短路径规划:从数学建模到MATLAB实现

📅 2026/8/27 7:23:27
基于图论与几何优化的机器人最短路径规划:从数学建模到MATLAB实现
1. 项目概述从一道经典赛题到算法实战的深度复盘最近在整理历年数学建模竞赛的经典题目时2012年高教社杯全国大学生数学建模竞赛的D题“机器人避障问题”又一次引起了我的注意。这道题之所以经典不仅仅是因为它出现在国赛的舞台上更因为它完美地融合了几何优化、图论算法和动态规划等多个数学与计算机科学的核心领域为参赛者提供了一个绝佳的、从理论建模到代码实现的综合演练场。题目描述了一个在平面区域内存在多个圆形障碍物的场景要求我们为机器人设计一条从起点到终点的最短无碰撞路径。这听起来像是机器人学或游戏AI中的寻路问题但其本质是一个带约束的非线性优化问题。我之所以想专门写一篇关于这道题目的深度解析是因为我发现很多同学在初次接触时容易陷入两个极端要么被复杂的几何约束吓到只做粗略的近似要么直接调用现成的路径规划库却对背后的数学模型一知半解。实际上这道题提供了一个绝佳的机会让我们亲手搭建从问题分析、模型建立、算法设计到程序实现的完整链条。通过这篇复盘我希望不仅能带你重温这道经典赛题的解题思路更能分享如何将严谨的数学模型转化为高效、可靠的MATLAB代码并深入探讨那些在论文中可能一笔带过、但在实际编程中却至关重要的“坑”与技巧。无论你是正在备赛的数模新手还是对路径规划算法感兴趣的开发者相信这些从实战中沉淀下来的经验都能带来启发。2. 问题核心与建模思路拆解化繁为简的艺术面对“机器人避障问题”第一步也是最重要的一步就是准确理解题意并将其转化为可计算的数学模型。题目通常会给定一个二维平面上面标有起点O(0,0)、终点A以及若干个圆心位置和半径已知的圆形障碍物。机器人的尺寸被简化为一个点即质点模型但其安全距离要求路径上每一点到所有障碍物圆心的距离必须大于障碍物的半径通常加上一个安全裕量。我们的目标是找到一条连接起点和终点的最短路径。2.1 核心难点与关键洞察这道题的核心难点在于障碍物导致的非凸可行域。由于障碍物是圆形的它们将整个平面分割成了“可通行”和“不可通行”的区域。最短路径不可能是一条简单的直线因为它会穿过障碍物。那么最短路径可能是什么形状呢这里就需要一个关键的几何洞察在平面中绕过圆形障碍物的最短路径必然由直线段和与障碍物圆周相切的圆弧段组合而成。为什么这可以从最优化理论中的“最速下降”或“变分法”角度理解但一个更直观的解释是假设路径上有一点紧贴着障碍物边界即满足距离等于安全半径那么为了保持路径最短在绕过障碍物时路径会尽可能“擦着”障碍物边缘走而“擦着边缘走”在几何上就是沿着圆周的切线方向运动。因此全局最短路径可以看作是依次经过起点、一系列“关键点”如切点、交点、终点的折线其中某些线段是直线某些段落是沿着障碍物边界的圆弧。基于这个洞察我们的建模思路就从在连续平面上搜索转化为在一个有限的候选点网络中搜索。这些候选点包括起点和终点。所有障碍物之间的公切点外切线和内切线。障碍物与从起点/终点发出的射线之间的切点。这样一来一个连续的、无限维的路径优化问题就被离散化为一个图论中的最短路径问题。我们将每一个候选点看作图的“节点”将连接两个节点且不与任何障碍物内部相交的直线段或圆弧段看作图的“边”并赋予其实际长度作为“权重”。那么原问题就等价于在这个图中寻找从起点节点到终点节点的权重和最小的路径。2.2 模型建立的具体步骤环境建模与预处理首先在MATLAB中定义所有障碍物的圆心坐标(xi, yi)和半径ri。通常我们需要将障碍物半径加上一个安全阈值例如机器人半径或安全裕量delta得到膨胀后的障碍物半径Ri ri delta。所有距离计算都基于Ri进行。关键点节点生成起点与终点直接作为节点。切点计算这是整个建模的算法核心。需要计算点到圆的切点从起点O或终点A到每个膨胀障碍物的两条外切线切点。圆到圆的公切点计算任意两个膨胀障碍物之间的四条公切线两条外切线两条内切线的切点。这里涉及大量的解析几何计算是代码实现中容易出错的地方。将所有计算得到的切点去重后与起点、终点共同构成节点集合V。可行边边的构建与权重计算对于任意两个节点u和v判断连接它们的线段或弧段是否“可行”。情况A直线边。如果两点连线是一条直线即两点均不在同一个圆的边界上或者虽在同一个圆上但连线是弦而非弧则需要判断这条直线段是否穿过任何障碍物的内部。这可以通过计算线段到每个障碍物圆心的最短距离是否大于该圆的膨胀半径Ri来判断。如果对所有障碍物都安全则这条边是可行的权重w(u,v)就是两点间的欧氏距离。情况B圆弧边。如果两个节点恰好是同一个膨胀圆上的两个切点那么它们之间可能存在一条沿着该圆边界的可行圆弧路径。需要确定是走优弧大于半圆还是劣弧小于半圆通常最短路径会选择劣弧。此圆弧的权重就是其弧长计算公式为R * theta其中R是膨胀半径theta是两点与圆心形成的圆心角取小于π的值。遍历所有节点对构建出边集E和对应的权重矩阵。最短路径搜索现在我们得到了一个带权有向图如果所有边可双向通行则是无向图。使用经典的最短路径算法如Dijkstra算法或A*算法即可求出从起点到终点的最短路径。由于节点数关键点不会太多通常几十到上百个Dijkstra算法完全够用且实现简单。路径平滑与输出算法给出的路径是一个由节点序列构成的折线包含直线段和圆弧段。我们需要将这个序列翻译成连续的路径描述例如列出每一段的类型直线/圆弧、起点终点、圆心对于圆弧、方向等并计算出总路径长度。注意在实际竞赛中有一个非常重要的细节常被忽略——路径的可行性需要连续验证。仅仅检查线段的两个端点是否在障碍物外是不够的必须确保整条线段上的所有点都满足避障条件。对于直线段这等价于验证线段到圆心的最小距离大于半径。一个高效的判断方法是计算圆心到线段所在直线的垂足如果垂足落在线段上则最小距离就是垂足到圆心的距离否则最小距离取线段两端点到圆心距离的较小值。3. 算法实现核心MATLAB代码细节与避坑指南将上述数学模型转化为MATLAB代码是一个系统工程。下面我将分模块解析关键代码实现并分享那些在论文里看不到的“踩坑”经验。3.1 数据结构设计与初始化清晰的代码结构是成功的一半。我建议定义以下主要数据结构% 1. 障碍物定义 obstacles struct(center, [], radius, [], expanded_radius, []); % 例如 obstacles(1).center [x1, y1]; obstacles(1).radius r1; obstacles(1).expanded_radius r1 delta; % 2. 节点定义 nodes struct(pos, [], type, , associated_circle, []); % pos: [x, y]坐标 % type: start, goal, tangent_point % associated_circle: 对于切点记录它属于哪个障碍物索引方便后续判断圆弧边 % 3. 图结构 % 使用邻接矩阵或邻接表存储。对于节点数N不太大的情况N*N的邻接矩阵更直观。 N length(nodes); adj_matrix inf(N, N); % 初始化权重为无穷大 % 如果节点i和j之间存在可行边adj_matrix(i, j) 边权重3.2 切点计算几何计算的精度陷阱计算切点是整个代码中最容易出bug的部分。以计算点P到圆C圆心Oc半径R的外切点为例。理论公式向量OP P - Oc。距离d norm(OP)。切点T满足OT与OP的夹角为phi acos(R/d)并且OT相对于OP旋转了±phi角度。MATLAB实现示例function [T1, T2] pointCircleTangent(P, Oc, R) OP P - Oc; d norm(OP); if d R error(Point is inside or on the circle, no tangent exists.); end phi acos(R / d); % 计算OP的方向角 base_angle atan2(OP(2), OP(1)); % 计算两个切点的方向向量从圆心指向切点 dir1 [cos(base_angle - phi), sin(base_angle - phi)]; dir2 [cos(base_angle phi), sin(base_angle phi)]; T1 Oc R * dir1; T2 Oc R * dir2; end避坑指南1数值稳定性。当点P非常接近圆时d ≈ Racos(R/d)的计算可能因浮点误差导致R/d略大于1从而引发acos定义域错误。务必添加一个容差处理ratio R / d; if ratio 1 ratio 1 1e-10 ratio 1; % 或直接认为切点就是OP方向的单位向量乘以R end phi acos(ratio);避坑指南2圆到圆公切线的复杂性。计算两个圆之间的公切线涉及更多情况外离、相交、内含。网上有很多现成的几何公式但直接套用时务必注意坐标系的变换和角度范围的判断。一个稳健的方法是先计算两圆心的向量和距离然后通过解三角形使用asin和acos求出公切线与圆心连线的夹角最后再旋转得到切点方向。这部分代码较长建议单独写成函数并进行充分的单元测试用简单的图形如两个圆心在x轴上的圆验证切点坐标是否正确。3.3 可行性判断高效的几何碰撞检测对于任意两个节点A和B构成的线段我们需要判断它是否与所有障碍物膨胀后相交。核心函数isSegmentSafe(A, B, obstacles)function safe isSegmentSafe(A, B, obstacles) safe true; for k 1:length(obstacles) Oc obstacles(k).center; R obstacles(k).expanded_radius; % 计算圆心到线段AB的最短距离 [dist, ~] pointToSegmentDistance(Oc, A, B); if dist R - 1e-6 % 同样加入微小容差避免边界情况误判 safe false; return; end end end关键子函数pointToSegmentDistance计算点P到线段AB的距离。function [dist, foot] pointToSegmentDistance(P, A, B) % 向量表示 AB B - A; AP P - A; BP P - B; % 计算投影标量 t dot(AP, AB) / dot(AB, AB); if t 0 % 投影在A点之外最近点是A foot A; dist norm(AP); elseif t 1 % 投影在B点之外最近点是B foot B; dist norm(BP); else % 投影在线段内部 foot A t * AB; dist norm(P - foot); end end注意在判断dist R时强烈建议使用dist R - eps或dist R - 1e-6而不是dist R。这是因为浮点计算存在误差一个理论上相切dist R的路径计算出的dist可能略小于或略大于R。使用一个小的负容差可以确保路径是严格安全的避免因数值误差导致路径“擦碰”障碍物而被误判为不可行。3.4 图构建与最短路径搜索构建邻接矩阵for i 1:N for j i1:N % 无向图只计算一半 node_i nodes(i); node_j nodes(j); % 判断是否属于同一圆且为切点对可能构成圆弧边 if strcmp(node_i.type, tangent_point) strcmp(node_j.type, tangent_point) ... node_i.associated_circle node_j.associated_circle % 计算圆弧长度 circle_idx node_i.associated_circle; Oc obstacles(circle_idx).center; R obstacles(circle_idx).expanded_radius; % 计算向量夹角 v1 node_i.pos - Oc; v2 node_j.pos - Oc; theta acos(dot(v1, v2) / (norm(v1)*norm(v2))); % 取劣弧对应的圆心角可能还需要判断圆弧方向是否与路径方向一致 arc_angle min(theta, 2*pi - theta); weight R * arc_angle; % 还需要判断这条圆弧路径是否会被其他障碍物阻挡通常认为紧贴当前圆边界是安全的但严谨起见可以采样圆弧上的点进行碰撞检测。 if isArcSafe(node_i.pos, node_j.pos, Oc, R, obstacles, circle_idx) adj_matrix(i, j) weight; adj_matrix(j, i) weight; end else % 判断直线边 if isSegmentSafe(node_i.pos, node_j.pos, obstacles) weight norm(node_i.pos - node_j.pos); adj_matrix(i, j) weight; adj_matrix(j, i) weight; end end end end使用MATLAB内置的graph和shortestpath函数进行最短路径搜索非常方便% 创建图对象 G graph(adj_matrix, upper); % upper因为我们的邻接矩阵是对称的只存了上三角部分 % 计算最短路径起点和终点在nodes列表中的索引假设为start_idx和goal_idx [path_idx, path_len] shortestpath(G, start_idx, goal_idx);3.5 路径可视化让结果一目了然结果可视化是数模论文的亮点也是调试代码的利器。figure; hold on; axis equal; grid on; % 1. 绘制障碍物 for k 1:length(obstacles) viscircles(obstacles(k).center, obstacles(k).radius, Color, k, LineWidth, 1); % 绘制膨胀后的障碍物虚线 viscircles(obstacles(k).center, obstacles(k).expanded_radius, Color, r, LineStyle, --, LineWidth, 0.5); end % 2. 绘制所有候选节点 scatter([nodes.pos].x, [nodes.pos].y, 20, b, filled); % 3. 绘制最短路径 path_nodes nodes(path_idx); for s 1:length(path_idx)-1 i path_idx(s); j path_idx(s1); node_i nodes(i); node_j nodes(j); % 判断是直线还是圆弧并绘制 if adj_matrix(i, j) norm(node_i.pos - node_j.pos) % 直线 plot([node_i.pos(1), node_j.pos(1)], [node_i.pos(2), node_j.pos(2)], r-, LineWidth, 2); else % 圆弧需要根据圆心和起止点绘制圆弧段 circle_idx node_i.associated_circle; % 假设关联同一个圆 Oc obstacles(circle_idx).center; R obstacles(circle_idx).expanded_radius; % 计算起止角度 angle_start atan2(node_i.pos(2)-Oc(2), node_i.pos(1)-Oc(1)); angle_end atan2(node_j.pos(2)-Oc(2), node_j.pos(1)-Oc(1)); % 绘制圆弧注意角度方向 theta linspace(angle_start, angle_end, 100); x_arc Oc(1) R * cos(theta); y_arc Oc(2) R * sin(theta); plot(x_arc, y_arc, r-, LineWidth, 2); end end % 标记起点终点 plot(nodes(start_idx).pos(1), nodes(start_idx).pos(2), go, MarkerSize, 10, MarkerFaceColor, g); plot(nodes(goal_idx).pos(1), nodes(goal_idx).pos(2), mo, MarkerSize, 10, MarkerFaceColor, m); title(sprintf(最短避障路径规划结果 (总长: %.4f), path_len)); legend(障碍物, 膨胀边界, 候选节点, 最短路径, 起点, 终点); hold off;4. 从理论到实践常见问题与调试技巧实录即便思路清晰在具体实现时也一定会遇到各种问题。下面是我在复现过程中遇到的一些典型问题及解决方法这些是纯粹的“实战经验”。4.1 问题一路径“穿墙而过”碰撞检测失效现象算法找到的路径在可视化图中明显穿过了某个障碍物。排查检查膨胀半径首先确认是否将安全裕量delta加到了障碍物半径上。膨胀半径R_expanded r delta才是碰撞检测的依据。验证isSegmentSafe函数单独写一个测试脚本创建一个简单的场景比如一个障碍物和一条确定会相交的线段手动调用isSegmentSafe并输出计算出的距离dist和膨胀半径R。检查判断逻辑dist R是否正确。检查浮点容差如前所述将判断条件改为dist R - 1e-6。有时候因为计算误差一条理论上相切的线段其dist可能比R小一个极小的量如1e-15导致被误判为安全。采样验证对于可疑的线段可以在代码中增加采样点验证。在线段上均匀取10个点计算每个点到所有障碍物圆心的距离看是否有任何一个点距离小于对应膨胀半径。4.2 问题二最短路径不是全局最优甚至很奇怪现象算法找到的路径长度明显不是最短或者路径绕了远路甚至包含不必要的回头。排查检查节点集合首先可视化所有生成的候选节点切点。看看是否遗漏了关键的切点特别是从起点/终点到障碍物的切点以及障碍物之间那些可能构成“捷径”的公切点。如果节点缺失图的结构就不完整自然找不到最优路径。检查边的可行性判断可能某条本应存在的、更短的边被错误地判定为“不可行”。可以临时注释掉碰撞检测让所有节点之间都用直线连接运行最短路径算法。如果此时得到了一条更短的直线路径当然会穿障那么问题就出在碰撞检测过于严格错误地拒绝了一些本可行的边。重点检查那些被拒绝的边对应的障碍物和距离计算。检查圆弧边的权重确认圆弧边的权重计算是否正确。弧长公式L R * theta中的theta必须是弧度制且是劣弧对应的圆心角。如果误用了优弧权重会变大导致算法倾向于选择更长的直线路径。验证图的结构将构建好的邻接矩阵adj_matrix可视化使用spy函数查看非无穷大元素的位置或输出边的列表检查图的连通性。确保起点和终点在图中有边相连至少通过某些中间节点。4.3 问题三算法运行速度慢对于障碍物多的情况效率低下现象当障碍物数量增加到十几个或几十个时程序运行时间显著变长。分析瓶颈主要在两个环节切点计算计算所有障碍物两两之间的公切线复杂度是O(n^2)其中n是障碍物数量。对于每个障碍物对需要计算最多4组切点。当n20时障碍物对就有190个切点计算量很大。边的可行性判断构建图时需要判断O(m^2)条边的可行性其中m是节点数量m远大于n。每条边的判断又需要遍历所有障碍物n个。因此总复杂度约为O(m^2 * n)。优化策略空间剪枝在判断线段与障碍物是否碰撞时可以先进行快速的空间包围盒测试。计算线段的最小外包矩形如果一个障碍物的圆心加上半径后的外包圆与该矩形相离则该障碍物绝对不可能与线段相交可以直接跳过。这可以过滤掉大量明显无关的障碍物。局部构图并非所有障碍物都与当前线段相关。可以只为每个节点关联其“邻近”的障碍物例如以节点为中心一定半径范围内的障碍物。在判断边(u,v)时只检查与u和v都邻近的障碍物的交集。这需要建立空间索引如网格划分或四叉树实现起来稍复杂但对于大规模场景效果显著。近似算法如果对最优性要求不是绝对的可以考虑基于采样的路径规划算法如快速随机树RRT或其变种RRT*。这类算法在高维或复杂环境中效率更高虽然不能保证数学上的最短但能快速找到可行且较优的路径。这在后续的模型改进或扩展中是一个方向。4.4 问题四圆弧路径的“方向”歧义现象当两个切点位于同一个圆上时存在两条圆弧连接它们优弧和劣弧。算法可能错误地选择了更长的优弧或者选择的弧段在实际行走中需要机器人反向绕行不符合运动方向。解决方案明确选择劣弧在计算圆心角theta后直接使用min(theta, 2*pi - theta)作为弧长计算的依据。这确保了权重是较短的那条弧。考虑路径方向一致性在构建图时可以将圆弧边设为有向边。定义圆弧的方向为从路径上游节点到下游节点的绕行方向。计算圆心角时使用向量的叉积或角度差函数如atan2的差值来确定从起点切点到终点切点的有向角度范围在(-2π, 2π)。这样权重就是沿该方向行走的弧长。在最短路径搜索时需要确保前后两段路径在连接点处的方向是平滑的例如直线进入切点时的方向与圆弧起点的切线方向一致。这涉及到更复杂的“曲率连续”路径规划在原题基本要求之上但可以作为模型亮点。5. 模型评价、改进与扩展思考完成基本模型的实现后一篇优秀的数模论文还需要对模型进行评价并讨论其优缺点和改进方向。5.1 模型优点精确性基于几何和图论的方法在离散化的关键点网络中能搜索到理论上的最短路径全局最优解只要关键点集完备。概念清晰将复杂的连续空间避障问题转化为经典的图最短路径问题模型结构清晰易于理解和实现。可扩展性该框架可以扩展。例如障碍物形状可以从圆形扩展到凸多边形此时关键点变为多边形的顶点机器人模型可以从质点扩展到有尺寸的圆形或矩形通过配置空间法C-space进行转化。5.2 模型局限性计算复杂度关键点尤其是公切点的数量随着障碍物数量呈平方增长导致图的规模急剧扩大。构建所有边并进行碰撞检测的成本较高不适合实时性要求很高的动态环境。对凹形障碍物或复杂形状处理困难本模型依赖于“最短路径由直线和与障碍物边界相切的曲线组成”这一假设这只对凸障碍物严格成立。对于凹形障碍物最短路径可能需要在凹槽内“碰壁”这超出了当前模型的范畴。未考虑动力学约束模型只规划了几何路径没有考虑机器人的速度、加速度、最小转弯半径等动力学约束。实际机器人尤其是车辆模型无法瞬时改变方向需要满足曲率连续。5.3 可能的改进与扩展方向引入启发式搜索在构建图时可以使用A*算法。设计一个启发式函数h(n)例如当前节点到终点的欧氏距离直线距离这可以显著减少需要探索的节点数量加快搜索速度。路径平滑化通过图搜索得到的最短路径是由直线段和圆弧段组成的折线在连接点处可能不是切线连续即存在尖角。可以对路径进行后处理平滑例如使用B样条曲线或贝塞尔曲线在满足避障约束的前提下使路径一阶连续切线连续甚至二阶连续曲率连续更符合机器人的运动学特性。处理动态或不确定环境如果障碍物位置不确定或会移动可以将模型扩展为基于概率的路线图PRM或快速随机树RRT并结合传感器信息进行在线重规划。多目标点遍历旅行商问题TSP如果题目要求机器人依次访问多个目标点问题就变成了在避障约束下的旅行商问题。可以先在关键点图中求出任意两点间的最短避障距离形成一个完全图然后在此图上用动态规划或遗传算法求解TSP。回顾整个解题过程从最初的几何洞察到中间的图模型构建再到最后的算法实现与调试每一个环节都充满了挑战和乐趣。这道2012年的赛题之所以历久弥新正是因为它像一座微型的桥梁连接了数学理论、算法设计与工程实践。我个人的体会是在数模竞赛中拥有一个清晰的、可计算的模型框架远比追求复杂的算法更重要。先把“基于关键点和图搜索”这个主干打牢固确保它能正确运行然后再去考虑优化、扩展和美化。在代码实现中几何计算的鲁棒性和碰撞检测的准确性是两大基石多花时间编写测试用例来验证这些基础函数会为后续的调试节省大量时间。最后一幅信息丰富、直观的可视化图永远是论文中最有力的语言。