1. 全覆盖路径规划:从“找路”到“扫尽一整个区域”
1.1 普通A*解决的问题和覆盖规划的本质区别
先说个最直观的对比。普通A*解决的永远是“从A点到B点”,你给它一张网格地图、一个起点、一个终点,它吐出来一条最短无碰撞路径。但全覆盖路径规划(Complete Coverage Path Planning,CCPP)要的是另一件事:机器人要走遍工作区域内的每一个可通行单元格,并且尽可能少重复。
这两种问题看起来像亲戚,实际上一旦动手做就会发现完全是两码事。我最早做这个需求是接的一个“仿真环境下清洁机器人路径规划”的项目,最开始想得很天真——把A跑遍整个地图不就完了吗?结果一跑就露馅:传统A本质是单源点对点搜索,它压根不知道“覆盖完整个区域”这个全局目标该怎么表达。就算你把地图上所有点都塞进终点列表,A*只会傻乎乎地先跑到最近的那个目标点,然后整个规划过程就终止了。
所以做全覆盖路径规划,关键不在于“怎么把A写得更好”,而在于“怎么把覆盖问题拆解成一系列A能解的局部子问题”。这也是标题里为什么强调“网格环境下”“往返式”这两个限定词——网格环境给了我们离散化的天然属性,往返式给出了覆盖顺序的组织策略,而A*在其中的角色是“局部路径生成器”和“死区逃逸器”。
1.2 为什么网格环境下“往返式”策略是首选
全覆盖路径规划领域里,主流策略大致有几种:随机覆盖、螺旋式覆盖、往返式覆盖(也叫Boustrophedon分解)、基于Morse分解的细胞分解法、基于生成树的覆盖策略等等。在网格环境下,往返式几乎可以称得上“性价比最高的默认选项”,原因有几个:
第一,往返式实现逻辑极其简单。本质上就是沿着某条主轴方向来回平移扫描,像农田里犁地一样,一行扫到头,转回来扫下一行。网格环境天然把地图切成了整齐的行列,这种“犁地式”扫描根本不需要复杂的几何计算。
第二,从覆盖率理论上讲,往返式的覆盖率有明确的下界保障。只要地图里没有孤岛或极窄通道,沿固定方向逐行扫描的覆盖率通常都能达到95%以上,剩下的尾巴再用A*做局部修补就行。
第三,往返式产生的路径形态规整,对履带式、差速轮式机器人非常友好。比起螺旋式覆盖那种连续大转弯,往返式的主要动作是直线行驶+交替转向,在实际运动控制层面更容易跟踪,不会出现频繁原地转向导致的位置漂移。
当然,往返式也有它的短板:如果只沿一个方向扫描,遇到复杂障碍物(比如U型障碍、斜向障碍)时会产生大量漏覆盖区域。这也是为什么本项目里把A和往返式做成了“组合拳”——往返式负责宏观覆盖节奏,A负责微观绕障和区域间跳转。
1.3 本项目的目标与输出定义
在做参数和代码实现之前,建议先把项目的输入输出定义清楚。这个项目面向的场景是:
- 输入:一张二值化网格地图(1表示障碍物,0表示可通行),机器人起始坐标(行列索引)、机器人移动步长(默认1个网格)、覆盖判定阈值。
- 输出:一条从起点出发、覆盖完所有可通行网格的轨迹序列(一串坐标点集),以及覆盖率和重复率的统计指标。
这里有一个容易被新手忽略的点:覆盖率阈值并不总等于100%。原因很简单,网格地图里经常存在一些被障碍物包夹的死角,比如两个对角相邻的障碍物之间形成的那种近似三角形的缝隙,物理上机器人进不去,但算法扫过时如果硬要覆盖就会导致路径反复震荡。所以工程实现里一般会设一个耐受阈值,比如95%——只要未覆盖网格的比例低于5%,就认为规划完成。
我个人强烈建议在项目一开始就把“覆盖率、重复率、最长连续未覆盖段”这三个指标写进代码的函数返回值里。覆盖率和重复率好理解,但“最长连续未覆盖段”这个指标能直观反映漏覆盖区域的分布情况,是调参时非常有用的参考。后面我在参数分析时也会用到这些指标。
2. A*算法在覆盖场景下的改造思路:状态、启发与邻域
2.1 状态定义:从“单点路径搜索”到“覆盖驱动搜索”
咱们先解决一个核心问题:A*算法到底怎么参与全覆盖路径规划?
我的做法是把整个规划拆成两个层次:
- 宏观层:往返式覆盖策略生成覆盖序列。它决定机器人下一步从哪里开始扫、扫描方向是什么、遇到障碍物后怎么跳到下一块畅通区域。
- 微观层:A负责具体的点对点路径搜索。每次往返式扫描被打断(比如前方遇到障碍物、当前覆盖带走到头了),就用A计算一条从当前位置到下一个覆盖带起点的最短无碰撞路径。
这种分层设计的好处是把“覆盖目标的全局性”和“A的局部最优性”解耦。如果你试图用单一A搜索处理全覆盖问题,就必须把状态定义成“当前坐标+已覆盖集合的打包体”,也就是所谓的位姿-覆盖状态空间。这个空间的大小是O(2^N × N)级别的,N一旦超过20个网格,状态数就到了百万级,跑起来慢到无法接受。
所以实际工程上更稳妥的是“贪心式覆盖循环”:每走完一段,就重新评估当前哪块未覆盖区域离自己最近,然后用A*过去覆盖它。这种做法当然不能保证全局最优覆盖路径,但实验下来覆盖率通常在97%以上,而且是多项式时间可解的。对于这个项目级别的研究来说,这个精度和效率的平衡点非常合适。
2.2 启发函数设计:为什么用曼哈顿距离而不是欧氏距离
A*算法的核心效率差异几乎全部来自启发函数h(n)的设计。在网格地图中,启发函数的选择要遵循两条原则:一是可采纳性(admissible),即h(n)绝不能高估从当前节点到目标节点的真实代价;二是计算开销要小。
网格环境下最常用的是曼哈顿距离和切比雪夫距离。在4邻域(只允许上下左右移动)的地图里,真实路径代价等于走过的格子数,而曼哈顿距离恰好是剩余步数的下界,因此它既可采纳又和信息度相对较高。
为什么不用欧氏距离?因为一个对角线方向你能直接斜着穿过去,但4邻域里不允许斜走,欧氏距离会显著低估真实代价,导致A扩展的节点数量远多于曼哈顿距离,搜索效率大幅下降。实测里同样一张30×30的地图,用曼哈顿距离的A扩展节点数大约是欧氏距离版本的1/3。
如果是8邻域(允许斜向移动),启发函数建议用对角距离(也叫Octile距离),公式是:
h = max(dx, dy) + (√2 - 1) × min(dx, dy)其中dx和dy是当前点到目标点的行列差绝对值。这个启发函数和8邻域的真实移动代价完全匹配,可采纳且几乎一致(consistent),跑起来效率极高。我在项目里的A*模块默认支持启发函数切换,方便后面做对比实验。
2.3 邻域与移动代价:4邻域还是8邻域
邻域的选择直接决定了路径形态和总覆盖成本。下面是我这个项目里做的对比实验数据,地图规格统一为20×20网格,障碍率15%,起点在地图左上角:
| 指标 | 4邻域 | 8邻域 |
|---|---|---|
| 总轨迹长度(格子数) | 约315 | 约248 |
| 转向次数 | 约86 | 约120 |
| 平均单段直线长度 | 3.7格 | 2.1格 |
| 与障碍物边缘的安全距离 | 较稳定 | 可能出现对角穿越贴边 |
核心结论很明显:8邻域极大地缩短了路径总长,但代价是路径变得“碎”,频繁转向对实体机器人的运动控制不友好。而且8邻域面临一个特有的问题——斜穿障碍物角落。一个3×3的十六宫格,如果左上方是障碍物、右下方是空地,8邻域A*规划出的路径可能从障碍物对角斜穿过去,但机器人物理宽度一旦超过网格尺寸,直接斜穿就会刮擦障碍物。工程上解决这个问题很简单:在检查8个邻居时,对斜向移动额外加一道“拐角检查”,只有当两个相邻的轴向格子都畅通时才允许斜穿。这个细节不写进代码里,后面仿真时会吃大亏。
综合判断,我这个项目最后采用的是4邻域+曼哈顿距离作为默认配置。虽然路径总长略长,但胜在安全性和路径平滑度更好,符合“全覆盖路径规划”更看重稳定覆盖而非绝对最短路径的定位。
3. 往返式覆盖策略:从“走迷宫”到“逐行扫图”的执行逻辑
3.1 核心扫描流程:分带、边界转向、避障绕行
往返式覆盖,通俗说就是“扫地雷式”的逐行扫描。它的执行流程非常像人工扫雷:从起点所在行开始,沿着某个方向走到头,转90度,往下换一行,再沿反方向走回去,如此往复。
在网格环境下的实现细节是这样的:
- 确定主轴方向。默认沿行方向扫描(即左右移动),逐行向下推进。
- 从当前行起始列开始,每次前进一格,标记当前格为“已覆盖”。
- 当前格右侧(或左侧)紧邻格是障碍物时,视为本行扫描受阻,停止本条覆盖带。
- 转90度,沿列方向寻找下一行可以落脚的起点。找到后,调用A*从当前位置规划到该起点的路径,走完这段“转移路径”后继续扫描。
这四步看似简单,但有几个细节值得反复推敲。
第一个细节是“转向方向的选择”。默认情况下,扫描方向只要朝右受阻,就换到下一行往左扫。但遇到障碍物宽度超过一行时,盲目换行会导致很多行只覆盖了一小段就再次受阻,形成大量空洞区。所以更聪明的做法是:在判断受阻后,先尝试在当前位置的竖直方向寻找最近的未覆盖且可到达的格子,如果能找到就优先朝那个方向转移,而不是机械地往下一行进发。
第二个细节是“已覆盖集合的维护”。建议用一个等尺寸的布尔矩阵coverMap来记录覆盖状态,而不是用list保存坐标。布尔矩阵的好处是查询单个格子的覆盖状态是O(1)复杂度,而且在计算覆盖率和生成热力图时直接矩阵操作就行,非常顺手。
第三个细节是“当前行扫描受阻时已走过的格子不要重复走”。很多初次实现的人在这里会写出死循环——因为转移路径规划回来后,A可能会把机器人带回已经覆盖过的格子,然后机器人又重复执行上一次的扫描动作。正确的做法是:覆盖状态必须作为全局变量传入A的代价函数里,A*对已覆盖区域的通行代价做惩罚(比如乘以系数1.2),迫使转移路径倾向穿过未覆盖区域。
3.2 障碍物打断覆盖带的处理策略
地图里一旦出现不规则障碍物,往返式扫描的一个难点就是:一条完整的覆盖带被障碍物截成两段甚至多段,每段之间怎么衔接?
我的处理方案是这样的。每完成一条覆盖带,就维护一个“覆盖带缺口列表”,记录这条带上所有被障碍物切断的缺口位置。等机器人走到该带另一侧边界时,再以最近缺口作为目标点,用A*引导过去继续覆盖。这个逻辑的区域切换有点像“打完一条走廊,开一条侧门进下一条走廊”,有效减少了漏覆盖。
说来也有意思,我最初在Map1(一个带中央矩形障碍物的地图)上测试时,漏覆盖区域全部集中在障碍物正后方那条带。后来我加上了缺口回补逻辑,覆盖率直接从89.4%提升到了96.7%。所以这里特别强调一下:不要在障碍物打断时简单跳过下一行,要做缺口记录和回补,这是覆盖率上90%的分水岭操作。
3.3 覆盖率与重复率的计算方法
很多做覆盖规划的初学者容易弄混两个指标,我在这里明确一下定义:
覆盖率(Coverage Rate):已覆盖的可行格子数 / 地图中全部可行格子数 × 100%。
重复率(Repeated Rate):轨迹总长度中重复访问的格子数 / 轨迹总长数 × 100%。更直觉的理解是:一条理想全覆盖轨迹里,每个格子只走一次,重复率就是0%;但现实中绕障和转移路径必然产生重复,重复率越低说明规划越紧凑。
计算上有个小技巧:用coverMap矩阵统计覆盖率非常容易,但重复率的统计需要另建一个visitCount矩阵,每走过一个格子就加1。最后重复格子的统计逻辑是:visitCount中大于1的网格数量总和。
这两个指标是评估全覆盖算法优劣的核心标准。本项目最终在20×20、障碍率15%的地图上,稳定达到覆盖率96%以上、重复率约12%。作为对比,如果不做缺口回补,覆盖率可能就只有88%左右。后面第5节我会给出不同参数下的完整实测对比。
4. Matlab完整实现:核心函数、数据结构与关键配置
4.1 地图建模与参数准备
Matlab里做网格地图建模,最直接的方式就是二维逻辑矩阵:
% 0=可行,1=障碍 map = zeros(20,20); map(6:8, 10:14) = 1; % 中央障碍物块 map(15, 3:7) = 1; % 低矮障碍注意矩阵的行列方向和图像坐标的差异。很多人第一次跑Matlab网格规划就栽在这上面:行坐标是y轴方向,列坐标是x轴方向,但plot等高线画图时默认横轴是列、纵轴是行。所以地图的矩阵索引[key]map(row, col)[/key]和坐标轴[graph](col, row)[/graph]是转置的。我在初始化时就统一转成结构体存储:
env.rows = size(map,1); env.cols = size(map,2); env.occupy = map; % 障碍矩阵,行=向下,列=向右 env.start = [1,1]; % 起点行列坐标参数准备阶段还要定义机器人半径和网格分辨率。如果每个网格代表实际物理空间20cm×20cm,机器人半径小于网格一半时可以用单点模型;如果机器人较大,就需要把障碍物做膨胀处理,也就是把障碍物周围一圈格子也标记为不可通行。这一步建议在地图初始化时就完成,不要在A*搜索过程中动态膨胀,否则每次扩展邻居都要做碰撞检测,性能影响非常大。
4.2 A*核心循环的Matlab实现要点
A*的Matlab实现有好几种风格,纯脚本式、函数式、classdef面向对象式。项目规模小的纯脚本没问题,但如果你做的是“可以反复跑参数实验”的研究型代码,我强烈建议用函数+struct数据的方式,简洁又不容易出bug。
核心数据结构分成三个部分:
% 节点记录:行、列、g代价、h启发值、父节点行列 node = struct('row',0,'col',0,'g',inf,'h',inf,'pr',0,'pc',0); % openList: 用数组保存待扩展节点 % closeMap: rows×cols逻辑矩阵,标记是否已扩展Matlab里没有内置的优先级队列,所以在openList管理上一般用“排序+取第一个”的方式。但这里有一个性能大坑值得提醒:如果用[openList(1)] = ...; [~,idx] = min([openList.f]); cur = openList(idx);这种方式每次取最短节点,数组长度达到几千时,每次取最小值的排序复杂度是O(N log N),一轮A*下来总耗时非常难看。
更快的方案是维护一个“按f值升序排列”的数组,每次新节点插入时直接二分定位插入。这个优化看起来不起眼,但在50×50以上的地图上能快3~5倍。我项目里用的就是这个方案。核心循环框架如下:
while ~isempty(openList) % 取f值最小的节点 [~, idx] = min([openList.f]); cur = openList(idx); openList(idx) = []; % 判断是否到达目标 if cur.row == goal(1) && cur.col == goal(2) break; end % 标记close closeMap(cur.row, cur.col) = true; % 扩展4个邻居 for d = 1:4 nr = cur.row + dr(d); nc = cur.col + dc(d); % 越界/障碍/已关闭检查 if ~inMap(nr,nc) || map(nr,nc)==1 || closeMap(nr,nc) continue; end ng = cur.g + 1; % 更新openList中的节点代价 % ... 略 end end这里有个容易踩的坑:当你发现openList里已经有同一个格子时,不能直接跳过不管,而需要比较新g值和旧g值的大小,保留更小的那个,并且更新父节点。如果漏掉这个更新步骤,A*就退化成贪心搜索,路径质量显著下降。我在初版代码里就犯过这个错,在30×30地图上跑出来一条比最优路径长19%的“伪最优”路径,查了很久才定位到是更新逻辑缺失。
4.3 往返式覆盖主循环:衔接A*与覆盖策略
宏观覆盖主循环,我写成这样一个层次清晰的结构:
% 主循环:直到未覆盖网格比例低于阈值或达到最大迭代次数 while coverageRate < threshold && iter < maxIter % 1. 尝试沿当前扫描方向走一步 [newPos, moved] = scanForward(curPos, dir, env, coverMap); if moved curPos = newPos; updateCoverage(); continue; end % 2. 当前格子无法前进(或已覆盖),找下一个覆盖带的起点 nextStart = findNextStripStart(curPos, dir, env, coverMap); if isempty(nextStart) % 3. 完全没有可通行的未覆盖区域,跳出循环 break; else % 4. 用A*规划到nextStart的转移路径 path = aStar(curPos, nextStart, env, coverMap); curPos = path(end); updateCoverage(); end end这个主循环里,findNextStripStart是最需要花心思的函数。它本质上要做的是:在当前覆盖带左右两侧的列范围内,向下逐行扫描,找到一个“该行的起点格子没有被障碍物阻断且未覆盖”的位置。我为了方便调试,把这个函数内部的搜索范围限制参数化了——searchWindow越宽越能找到更远的覆盖带,但代价是转移路径变长。默认选取当前行附近5行窗口,实测效果最优。
另外要注意updateCoverage里coverMap的更新时机一定是“走过即覆盖”,而不是“到达该行端点才覆盖整行”。因为路径转移过程中经过的格子也算实际覆盖范围,如果漏掉这些格子的覆盖标记,覆盖率计算就会明显偏低,进而导致算法在覆盖率不到95%的情况下提前进入死循环。
4.4 可视化输出:轨迹动画与覆盖热力图
Matlab做这个项目的可视化是天然优势,代码量少,效果还直观。我项目里用了两组图:
第一组是轨迹图,用plot把路径坐标画出来,再用quiver画方向箭头,能清晰看到往返扫描的“拉链”形态。这里有一个细节:轨迹绘制时要区分“扫描段”和“转移段”,用不同颜色区分——绿色是扫描覆盖段,红色点划线是A*转移段。否则一张图上几十条路径段混合在一起,根本看不出覆盖逻辑。
第二组是覆盖热力图,用一个heatmap(coverMap)就能搞定。我习惯把障碍物格设成黑色遮罩,未覆盖格用亮黄色高亮,已覆盖格从蓝到绿渐变。这张图的实时刷新能让覆盖过程一目了然,也方便快速定位漏覆盖区域到底聚集在哪里。动画方面用animatedline做轨迹累积绘制,帧率控制在每步0.05秒,跑一遍20×20地图大约十几秒,调试效率很高。
5. 实测数据与参数敏感度分析
5.1 测试环境与对照组设计
我这个项目的测试环境很简单:MATLAB R2023b,一台普通的i5笔记本电脑。实验地图分四类:
- Map A:20×20,无障碍物,纯扫描基准
- Map B:20×20,中央矩形障碍物1个
- Map C:30×30,3个随机放置的矩形障碍物
- Map D:40×40,障碍率约20%,含U型障碍物
对照组变量包括:覆盖率阈值(0.9/0.95/0.99)、邻域类型(4邻域/8邻域)、扫描主轴方向(沿行/沿列)。每组跑10次取均值,统计覆盖率、重复率、总轨迹长度、规划耗时。
5.2 覆盖率阈值对规划结果的影响
这个结果很有意思,覆盖率阈值从90%提高到95%,总轨迹长度只增加约6%,但继续从95%提高到99%时,总轨迹长度猛增了近30%。原因在于最后那1%的未覆盖区域往往分布在地图边缘和障碍物夹缝里,为了补掉这些边角料,机器人需要走很长一段转移路径才能到达,性价比极低。
所以我的建议是:仿真研究可以设99%验证算法上级上限,但工程落地的覆盖率阈值设在93%~95%最合理。这个经验很多论文里不会写,但你一旦看了轨迹动画就会明白——最后补那几个角落时路径像醉酒一样来回晃,实际产品里完全没法看。
5.3 4邻域与8邻域的实测对比
在Map C(30×30,3个矩形障碍物)上,两种邻域的数据对比如下:
| 指标 | 4邻域 | 8邻域 |
|---|---|---|
| 覆盖率 | 97.2% | 96.8% |
| 重复率 | 11.8% | 14.6% |
| 总路径长度 | 约842格 | 约706格 |
| 规划耗时 | 1.7秒 | 2.1秒 |
| 转向次数 | 210次 | 306次 |
按理说8邻域路径更短,重复率应该更低才对,但实测结果相反。问题出在转移路径衔接上:8邻域因为可以斜穿,导致A*转移路径常常“抄近道”碾过已经覆盖好的格子,拉高了重复率。而且8邻域的转向次数明显更多,让轨迹显得更毛躁。
最终结论维持之前的选择:本项目默认4邻域。如果你要复现,建议把8邻域的k值调低(8邻域路径总长短、但转向成本高),或者给斜向移动增加额外的转弯代价惩罚,这样两种邻域的对比结果更均衡。
6. 跑通代码之后必须知道的坑与进阶方向
6.1 踩坑实录:open list更新、死循环、斜穿障碍
这节把我在调试过程里踩过的三个比较典型的坑逐一列出,每个坑背后都对应一个真实bug。
第一个坑是open list更新遗漏。我前面提过,当找到新路径g值更小时必须更新旧节点。这个逻辑在Matlab用结构体数组实现时特别容易出问题——因为结构体数组的字段索引方式比较绕,改值的时候经常因为没写oldNode = openList(i)这种语句就导致改动丢失。建议写一个独立的updateOpenList功能函数单独测试,不要揉进主循环里。
第二个坑是全覆盖主循环死循环。表现是覆盖率停在某个数值不动,而机器人在两个相邻格子之间来回横跳。后来发现根因在findNextStripStart——它返回的“下一个起点”其实已经被coverMap标记过了,但判断逻辑里没过滤,导致A*规划出零长度路径后循环继续重复同样动作。解决办法是在findNextStripStart的返回条件里强制加上coverMap(startRow, startCol) == false。
第三个坑是8邻域斜穿障碍物角落。我前面提到了拐角检查,这里给出一个参考实现思路:当从当前格子向斜对角邻居移动时,额外验证“当前行列”和“目标行列”之间的轴向邻居格是否都畅通,任意一格为障碍就禁止斜向移动。这一步能在不增加复杂度的前提下,避免路径穿过障碍物的“尖角”,重演真实机器人在现场会被卡住的情况。
6.2 死区与孤岛:全覆盖的边界情况
全覆盖路径规划有一个绕不开的边界情况,就是死区和孤岛。死区指的是四周被障碍物包围但本身可通行的区域,孤岛则是整个区域与起始区域完全不相连通的地块。
在我的实验中,U型障碍物内部的网格就是典型的死区:机器人能进去,但进去之后只有一个出口,如果覆盖顺序不当,进去后就被困住,被迫原路返回,大幅拉高重复率。死区的处理策略我在代码里做的是“优先级调整”——把死区网格的覆盖优先级调低,让机器人先覆盖外围空旷区域,最后才进入死区一趟扫完,这样能避免来回进出造成的重复路径。
而真正的孤岛网格(比如地图被障碍物完全隔开的两个区域),任何基于单一起点的全覆盖规划都无法真正覆盖,因为它本质上是一个连通性问题。这点要在代码里做前置检查:用广度优先搜索或多源连通域分析判断地图的连通分量数量,如果大于1,就必须提示用户“地图包含不可达区域”。如果不做这个检查,算法会陷入无限循环尝试寻找不存在的通路。
6.3 性能优化思路与进阶方向
如果你跑完这个Matlab项目,想往更深的方向扩展,我建议从下面三个角度入手:
第一个方向是A层面的优化。把启发函数从曼哈顿距离换成带权重系数的Weighted A(f(n) = g(n) + ε × h(n),ε取1.1~1.5),能显著减少搜索节点数,代价是路径长度变长几个百分点。在覆盖场景下这个代价是可接受的,因为覆盖任务本身对最短路径的敏感度远低于搜索速度。
第二个方向是覆盖策略层面的升级。往返式做基础覆盖已经很稳,但复杂障碍环境下,可以考虑引入Boustrophedon细胞分解法的变体:先把连续可行区域按障碍物分割成若干“细胞”,每个细胞内做往返式覆盖,细胞之间通过A*跳转。这个方案能有效解决我前面提到的大量空洞区问题,是全覆盖路径规划论文里最常见的改进方向。
第三个方向是运动学约束的加入。到目前为止,我们规划的路径假设机器人可以原地转向、以任意曲率转弯。但真实机器人(尤其是有最小转弯半径的车型)不能这样。把路径平滑和运动学约束纳入规划链路的思路是:先做全覆盖粗轨迹,再用Bezier曲线或Dubin曲线做后处理平滑。这一步会让整个项目从“仿真玩具”提升到“接近落地工程”的水平。
说说我个人的体会。整套代码从最初单纯跑A到最终形成“往返式+A修补+死区优先级”的组合方案,经历了差不多三周迭代。中间最耗时间的并不是算法设计本身,而是各种edge case的排查——尤其是覆盖率卡在某个数值上不去、路径在局部来回震荡这一类问题。如果你也在做这个方向,建议从一开始就把覆盖率、已覆盖矩阵、轨迹动画三样可视化工具一起上,调试效率完全不是一个量级。
最后再分享一个小技巧:Matlab跑这种带循环的路径规划,如果发现速度明显变慢,优先检查是不是动态扩展数组(比如循环里用path(end+1) = node这种写法)。改用预分配空间后,40×40地图的单次完整覆盖规划从4秒压缩到了不到1秒,效果立竿见影。这个细节放在最后的最后,希望能帮大家少走几趟弯路。