简介:这套MATLAB代码围绕Dijkstra算法在格栅地图上的最短路径搜索展开,适合机器人路径规划、智能车导航或算法课程设计场景。压缩包共4个文件,包含3个.m脚本和1个.mat数据文件:脚本涵盖主程序、核心算法实现与辅助函数,数据文件提供可通行的格栅地图矩阵,整体仅2KB,轻量且便于二次开发。已有1780人学习下载。代码采用邻接矩阵建模,通过维护距离向量与访问状态,并借助优先队列优化节点选取,最终利用前驱矩阵回溯最短路径。使用者可替换地图数据,直观验证Dijkstra算法在静态格栅环境中的寻路效果,也可对照代码理解初始化、迭代更新、邻居松弛与路径重构的完整流程,是入门路径规划与算法仿真的实用参考。
1. 为什么 Dijkstra 仍是格栅地图路径规划的首选定调算法
在格栅地图上做路径规划,很多人的第一反应是 A*,毕竟有启发式函数可以加速收敛。但当你需要对障碍物和非均匀代价做频繁调整时,A* 的启发式设计会带来额外的心智负担;Dijkstra 算法不依赖启发信息,只按累计代价一层层往外扩散,代价最小者先被确定,因此第一次带出终点时就是全局最优。这个特点让它在 MATLAB 里实现起来异常稳,尤其适合课程作业、算法答辩,以及给动态避障小车路径规划、无人机路径规划算法当基准线。当你需要实现 Dijkstra 算法求解格栅地图路径、交付可运行的 matlab 代码时,核心就是这张地图和那个扩展循环,把它讲透,后面再学 A*、JPS 都会顺很多。这篇文章不搬运某个压缩包里的具体实现,而是把这类工程最常见的建模方式、函数写法、参数调法和验证技巧完整过一遍。
2. 格栅地图建模与 Dijkstra 算法的 MATLAB 表示
2.1 把栅格地图变成带权图的两种常见做法
栅格地图本身是二维数组,0 代表障碍,1 代表可通行。Dijkstra 要在这个数组上找一条从起点到终点的最小代价路径,第一步是把地图转换成图。常见做法有两种:第一种是稀疏图,节点是每个格子,边是相邻格子的连接关系,MATLAB 里用graph()函数构造;第二种是隐式图,不把边存下来,计算时动态枚举邻居。对格栅地图我更推荐第二种,因为边和节点都排布规则,动态枚举省内存,也方便加各种检查条件,比如对角线穿墙判断。
隐式图的关键在于邻域集合。四邻域只有上下左右,每一步代价相同;八邻域还要处理对角方向,代价要按欧几里得距离乘上sqrt(2)。下面这段代码定义了两种邻域:
% 四邻域,偏移量是 [行偏移, 列偏移] neighbor4 = [-1 0; 1 0; 0 -1; 0 1]; % 八邻域,前 4 行还是上下左右,后 4 行是对角 neighbor8 = [-1 0; 1 0; 0 -1; 0 1; -1 -1; -1 1; 1 -1; 1 1];逻辑说明:偏移量按“行、列”排列,不是按“x、y”。这样可直接用在矩阵下标上。neighbor8里的行序不要求固定,只要保持后 4 行为对角即可。参数选择上,四邻域路径更长更多直角,适合只能前后左右移动的 AGV;八邻域路径更自然,但必须加斜穿检查,这点会在下文专门说明。
2.2 用矩阵存代价值,避开 MATLAB 稀疏图接口
Dijkstra 的循环里要反复查询“当前未确定节点中,谁的累计代价值最小”。如果用graph对象做,每次更新权重还要调用rmedge和addedge,语法冗长且容易被误解。我一般的做法是把代价值直接存成二维矩阵dist,尺寸与地图一致,初始值为Inf。这样既方便后续热力图显示,又能在计算时用矩阵下标直接定位节点。
dist = Inf(rows, cols); dist(start(1), start(2)) = 0; visited = false(rows, cols); parent = zeros(rows, cols, 2); parent(start(1), start(2), :) = [start(1), start(2)];这段代码定义了 Dijkstra 需要的三个核心数据结构:dist保存起点到每个格子的当前最短代价值;visited标记该格子的最短路是否已经确定;parent记录上一跳坐标,用于最后回溯整条路径。这里选择用三维矩阵而不是 cell 数组保存 parent,是因为三维矩阵在 MATLAB 的for循环里读写速度更快,回溯代码也更紧凑。在地图大于 1000x1000 时可以拆成parentRow和parentCol两个矩阵,避免第三维访问稍微变慢,但对一般格栅地图,三维写法最容易读。
2.3 边权设计:四邻域、八邻域与非均匀代价表
边权是 Dijkstra 的全部输入。如果地图就是 0/1 二值图,四邻域下每条边代价为 1,算出来的就是最短步数;八邻域下对角边要设成sqrt(2),否则会低估斜移代价,导致路径偏爱斜走。实际地图通常不是均匀的,草地、沙地、减速带都有不同通行成本。下面这张表是我处理地图时代价矩阵的常规设计:
| 地形类型 | 地图值 | 单步代价 | 说明 |
|---|---|---|---|
| 障碍 | 0 | 不可通行 | 扩展时直接跳过 |
| 普通路面 | 1 | 1 | 基准代价 |
| 草地 | 2 | 1.8 | 模拟速度下降 |
| 沼泽 | 3 | 3.5 | 只适合低速通过 |
读取这类代价矩阵时,只需要把costmap(nr,nc)当作边的权重;对角线方向乘上sqrt(2)后再乘该格值。代价矩阵里不能出现 0 但又不是障碍的值,否则“走不走都无所谓”会让算法产生很多等价路径。障碍值 0 要跟 Dijkstra 的初始化值Inf区分开,代码里判断不可通行时看的是地图值,而不是dist值,这个细节在后面第 4 章会继续展开。
3. 用 MATLAB 在格栅地图上跑通 Dijkstra 的最小代码
3.1 函数骨架输入与输出
我提供的实现叫dijkstra_grid,输入是代价地图、起点、终点和邻域类型。完整函数代码如下,你应该可以直接复制到本地运行。
function [path, totalCost] = dijkstra_grid(costmap, start, goal, neighborType) % DIJKSTRA_GRID 在格栅地图上用 Dijkstra 求最短路径 % costmap: 二维矩阵,0 表示障碍,正数表示通行代价 % start/goal: [row, col] 格式 % neighborType: '4' 或 '8' [rows, cols] = size(costmap); if costmap(start(1), start(2)) == 0 || costmap(goal(1), goal(2)) == 0 error('起点或终点必须在可通行区域'); end if nargin < 4 || isempty(neighborType) neighborType = '4'; end dist = Inf(rows, cols); dist(start(1), start(2)) = 0; visited = false(rows, cols); parent = zeros(rows, cols, 2); parent(start(1), start(2), :) = [start(1), start(2)]; if neighborType == '4' offsets = [-1 0; 1 0; 0 -1; 0 1]; diagMode = false; else offsets = [-1 0; 1 0; 0 -1; 0 1; -1 -1; -1 1; 1 -1; 1 1]; diagMode = true; end while true minVal = Inf; minIdx = []; for i = 1:rows for j = 1:cols if ~visited(i,j) && dist(i,j) < minVal minVal = dist(i,j); minIdx = [i,j]; end end end if isempty(minIdx) path = []; totalCost = Inf; return; end if minIdx(1) == goal(1) && minIdx(2) == goal(2) break; end visited(minIdx(1), minIdx(2)) = true; for k = 1:size(offsets, 1) nr = minIdx(1) + offsets(k, 1); nc = minIdx(2) + offsets(k, 2); if nr < 1 || nr > rows || nc < 1 || nc > cols continue; end if costmap(nr, nc) == 0 || visited(nr, nc) continue; end if diagMode && k > 4 dr = offsets(k, 1); dc = offsets(k, 2); if costmap(minIdx(1)+dr, minIdx(2)) == 0 || ... costmap(minIdx(1), minIdx(2)+dc) == 0 continue; end stepCost = costmap(nr, nc) * sqrt(2); else stepCost = costmap(nr, nc); end newDist = dist(minIdx(1), minIdx(2)) + stepCost; if newDist < dist(nr, nc) dist(nr, nc) = newDist; parent(nr, nc, :) = minIdx; end end end path = []; cur = goal; while any(cur ~= start) path = [cur; path]; cur = [parent(cur(1), cur(2), 1), parent(cur(1), cur(2), 2)]; end path = [start; path]; totalCost = dist(goal(1), goal(2)); end函数输入参数说明如下:
| 参数 | 类型 | 说明 |
|---|---|---|
costmap | double 矩阵 | 每个格子的通行代价,0 为障碍 |
start | 1x2 向量 | 起点[行, 列] |
goal | 1x2 向量 | 终点[行, 列] |
neighborType | char | '4'或'8' |
主循环里我现在没有在visited之前判断终点。依赖的是 Dijkstra 的性质:当终点从未扩展节点里以最小值被取出时,它的 dist 已被收敛。如果你把判断放在“弹出后更新邻居前”,也一样正确,但写法会让初学者误以为需要多扩展一轮。代码中每次寻找最小节点都要扫描全图,复杂度 O(V^2),对 200x200 地图完全够用;超过 400x400 建议使用堆优化。
3.2 主循环与终点判断:为什么可以先 break
主循环中有一行特别容易被误删:visited(minIdx(1), minIdx(2)) = true;如果漏掉,算法会反复扩展起点,因为起点的 dist 总是最小,最终导致死循环。所有由栅格地图派生的 Dijkstra 代码都必须保证:每个节点最多被扩展一次。当终点被挑出时,我们直接break;如果地图不存在通路,最后一个可达节点扩展完后minIdx为空,函数返回空 path 和Inf总代价。这里有一个常见的判断误区:有人在minIdx为空时用error直接报错,其实路径规划里“无路可达”是业务结果而不是异常,返回空数组更合适,调用脚本可以根据isempty(path)给出提示。
另一个要注意的是newDist < dist(nr,nc)更新条件。如果地图里有负边权,Dijkstra 会失效,但格栅地图的通行代价全为正数,所以可以放心使用。对于非均匀代价地图,stepCost使用的是目标格子的代价而不是当前格子的代价,这意味着“进入草地”才计费。如果你希望把“停在当前格子”也产生代价,需要在建图阶段先把当前格代价均匀到周围边的权重里。这个细节对泊车路径规划这类关心原地转弯代价的场景特别重要,能用 Dijkstra 跑通不代表模型正确,代价矩阵的语义要先定稿。
3.3 回溯路径与绘制脚本
回溯路径的代码在函数末尾。由于parent里存的是坐标,沿 parent 从终点一路向前即可。注意起点不会被更新,所以要手动设置parent(start) = start,否则回溯到起点后会取到[0,0],越界报错。绘制路径的脚本如下:
% visualize_dijkstra.m rng(7); costmap = ones(30,30); for k = 1:round(30*30*0.2) costmap(randi(30), randi(30)) = 0; end costmap(1,1) = 1; costmap(30,30) = 1; [path, totalCost] = dijkstra_grid(costmap, [1,1], [30,30], '8'); imagesc(costmap); colormap(gray); hold on; if ~isempty(path) plot(path(:,2), path(:,1), 'r.-', 'LineWidth', 2, 'MarkerSize', 12); title(sprintf('Dijkstra path, total cost = %.2f', totalCost)); else title('No path found'); end axis equal; axis tight;这段脚本用rng(7)固定随机种子,得到稳定可复现的地图。plot(path(:,2), path(:,1))因为图像显示时横坐标是列、纵坐标是行,所以交换前两列,否则路径会相对障碍物旋转 90 度。这里如果你的 MATLAB 版本比较旧,记得先按 MATLAB 安装教程确认 Image Processing Toolbox 已勾选,imagesc和colormap虽然不需要工具箱,但后面要用bwlabel时没有工具箱会直接报错。
4. 格栅地图 Dijkstra 的 4 个必调参数与常见坑
4.1 邻域数量与斜穿障碍角检查
参数neighborType决定路径形态。四邻域生成的路径只含直线段,几乎不会出现机器人原地转不过弯的情况;八邻域路径更短更自然,但必须处理对角线穿墙。处理方法是:从(i,j)走向(i+di,j+dj)时,要同时检查(i+di,j)和(i,j+dj)是否可通行。两者只要有一个是障碍,就禁止斜穿。不检查的代码长这样:路径直接从墙角“咬过去”,视觉上看好像比绕行短,但真实机器人会撞墙。检查代码在dijkstra_grid里面已经写好了,但你需要记住它与bwlabel的连通规则要统一:四邻域 Dijkstra 对应四连通;八邻域 Dijkstra 对应八连通。
4.2 地图分辨率、膨胀半径与代价归一化
栅格地图的每个格都有一个物理尺寸,比如 0.05 米。Dijkstra 算出来的路径是栅格中心的连线,直接下发到底盘前必须做膨胀。膨胀方法可以很简单:
% 障碍物向外扩张 robotRadius/cellSize 圈 radius = ceil(robotRadius / cellSize); mapDilated = imdilate(~costmap, strel('square', 2*radius+1)); costmapDilated = double(~mapDilated);注意这里先取反,因为imdilate对 1 表示障碍的区域扩张,得到的结果再取反还原成代价图。radius的单位是栅格数,计算时向上取整,宁大勿小。同一份代价图,分辨率越低、膨胀圈数越少,路径越贴近障碍物;分辨率越高、膨胀圈数越多,路径越保守但计算时间变长。地图代价的归一化也容易被忽视:如果普通路面是 1,草地是 1.8,而相邻划分非常粗糙,Dijkstra 可能宁可绕过很大一片草地也不走直线。先把代价归一化到[1, 10]区间,再让 Dijkstra 运行,得到的路径在高代价区域切换时不会突变。
| 参数 | 推荐值 | 影响 |
|---|---|---|
| neighborType | 8 带斜穿检查 | 路径平滑度与安全性折中 |
| 膨胀半径 | ceil(robotRadius/cellSize) | 防止路径贴墙 |
| 代价归一化 | 1~10 整型 | 避免高代价区被绕过 |
| bwlabel 连通性 | 与 Dijkstra 邻域一致 | 防止误判可达性 |
4.3 用 bwlabel 判断起点终点连通性
无解地图让 Dijkstra 把整个连通区域都耗尽才返回,性能上很亏。用bwlabel先做连通域标记,可以在算法开始前就结束:
cc = bwlabel(costmap > 0, 4); % 4 或 8 与 Dijkstra 邻域一致 if cc(start(1), start(2)) ~= cc(goal(1), goal(2)) path = []; totalCost = Inf; return; end这段预处理的时间复杂度是 O(rows*cols),虽然不会让整体变快多少,但能提供明确的“不可达”反馈。使用 4 邻域时bwlabel的第二参数必须传 4,否则会误判为可达。实际很多代码默认传 8,四邻域 Dijkstra 用户看到bwlabel没加参数,很容易出现结果为空还找不到原因的问题。
4.4 边权为 0 和 Inf 混用时的行为差异
最后一个常见坑在代码里没有直接显示,但挺隐蔽:dist初始值用Inf,地图障碍值用 0,这两个值混在一起使用时,千万别把障碍物写到dist里。正确做法是代价矩阵costmap里的 0 只负责判断跳过。如果你用isinf(dist)去判断是否访问过,那起点dist=0反而被认为是未访问,逻辑会错。另外,有的老代码会用NaN替代Inf表示不可达,但在比较<时NaN永远为 false,导致最小节点选择失效。因此统一用Inf,并在函数入口处把所有NaN转成1或Inf:costmap(isnan(costmap)) = 0;。这样就算地图由图像处理流程生成,也不会因为存在NaN让 Dijkstra 静默失败。
5. 从算法到交付:模块化打包与最短路径验证技巧
以“Dijkstra算法求解格栅地图路径matlab代码.rar”为交付物时,我会把文件拆成main.m、dijkstra_grid.m、visualize_dijkstra.m和README.txt。解压后直接运行main.m就能复现。为什么强调直接运行?MATLAB 的当前路径经常是破碎的,多级目录会让用户看到一堆未定义函数。把函数与脚本放同一目录,并在main.m开头用mfilename获取当前文件路径,再用cd(fileparts(mfilename('fullpath')))切过去,能省掉很多“为什么报错”的提问。
5.1 验证 Dijkstra 结果的三种方法
验证最优性靠肉眼不够。我常用的三种方法依次是:小地图手算、BFS 对照、距离变换交叉验证。小地图直接看 dist 矩阵;0/1 代价地图上让 Dijkstra 与bwdist比较;最后画代价热力图。下面这段脚本用bwdist来验证总代价:
% verify_path_cost.m clear; clc; costmap = ones(32,32); costmap(10:14, 16:18) = 0; % 障碍块 start = [2,2]; goal = [30,30]; [path, cost] = dijkstra_grid(costmap, start, goal, '4'); % 用图像距离变换做参考 D = bwdist(costmap > 0, 'quasi-euclidean'); refCost = D(goal(1), goal(2)) - D(start(1), start(2)); if abs(cost - refCost) < 1e-9 disp('cost check passed'); else error('cost mismatch: %.4f vs %.4f', cost, refCost); endbwdist计算每个格子到最近障碍的距离,距离场的差可以作为最短路径代价的参考。这个验证不依赖路径本身,只核对总代价,能快速发现边权设置错误。quasi-euclidean是折中的距离度量,与 Dijkstra 在四邻域下的结果近似,但不完全等于四邻域步数。因此容差要放宽松到 1e-3。
5.2 用代价热力图快速定位错误格子
把dist矩阵画成imagesc(dist),对障碍物旁边的高代价区域做目检。最常见的异常是:热力图里有几个格子颜色跟周围明显断层,那通常是parent更新时用了上一格的代价而不是目标格代价。这种情况单看路径很容易漏掉,热力图一眼就能看出来。另外,可以把path画在热力图上方,同时显示grid on,方便逐格检查路径是否穿过障碍角。若你后面要迁移到 C++,同样可以把这个 dist 矩阵导出成 CSV,在 MATLAB 里复用这段可视化逻辑。这套验证流程跑顺了,再往无人机路径规划、动态避障小车路径规划这些高级场景扩展时,你至少有底气说:底层最短路径是正确的。
本文还有配套的精品资源,点击获取