简介:这份PDF文档是卡内基梅隆大学Dave Ferguson等人撰写的《A Guide to Heuristic-based Path Planning》,面向机器人学、自动化与人工智能领域的学习者和研究者,系统梳理了启发式路径规划算法家族。内容从静态环境下的A算法讲起,延伸至动态重规划的D、限时优化的ARA*,以及兼顾实时性与适应性的AD*,并对比各类算法的核心特性、适用场景与优缺点,还给出算法选型时需权衡的环境动态性、计算资源、路径质量与响应时间等因素。资源包内仅含1个PDF文件,大小约1.72MB,便于直接阅读与检索。目前已有131人学习,适合希望建立路径规划算法全局认知、为科研或工程选型提供参考的读者,可从中获得算法原理对比、真实机器人系统应用经验及选型指南。
1. 启发式路径规划到底在解决什么问题
很多人第一次接触 Ferguson 等写的《A Guide to Heuristic-based Path Planning》,会以为又是一篇讲 A* 的综述。真正读进去才发现,它关心的不是“怎么把 A* 写出来”,而是“当搜索空间大到暴力枚举根本跑不动时,启发式函数该怎么设计、怎么调、怎么保证它不会把最优解带偏”。启发式路径规划的核心矛盾就一句话:启发式估得越乐观,搜索越快,但越容易绕远路;估得越保守,路径越短,但节点展开量爆炸。这篇指南把这条权衡线拆成了可操作的工程方法,适合做机器人导航、游戏寻路、无人机航迹、仓储 AGV 调度这类需要在线出路径的从业者。下面我按自己复现过的顺序,把选型、实现、参数和踩坑讲清楚。
2. 启发式函数怎么选:从可采纳性到一致性
2.1 可采纳性与一致性到底卡住了什么
可采纳性(admissible)说的是启发式估计永远不能大于真实剩余代价。只要满足这条,A* 找到的第一条到达目标的路径就是最优的。一致性(consistent)更强:对任意边 (u,v),要求 h(u) ≤ cost(u,v) + h(v)。一致性直接带来一个工程好处——节点第一次被弹出时它的 g 值已经最优,不需要重复开放。很多线上事故不是算法写错,而是启发式在某个局部偷偷高估了,导致最优解被剪掉,路径看起来“能走”,但代价比预期高一截。
Ferguson 等强调的启发式设计流程,我一般拆成三步:先确定状态空间用什么坐标和分辨率,再选一个下界函数(欧氏距离、曼哈顿距离、对角距离),最后检查它在障碍物附近是否仍然可采纳。栅格地图里最常见的错误是直接用欧氏距离当启发式,却忘了栅格移动是八邻域、每步代价不是 1,结果启发式量纲和真实代价对不上。
2.2 三种基础启发式的适用边界
| 启发式 | 公式 | 适用移动模型 | 是否可采纳 | 常见翻车点 |
|---|---|---|---|---|
| 曼哈顿距离 | |dx|+|dy| | 四邻域、代价 1 | 是 | 八邻域下高估,路径贴墙 |
| 欧氏距离 | sqrt(dx²+dy²) | 任意方向、连续空间 | 是 | 栅格代价非 1 时量纲错 |
| 对角距离 | max(dx,dy)+(√2−1)min(dx,dy) | 八邻域、直走 1 斜走 √2 | 是 | 斜向代价设错就失效 |
选型时先问自己:移动基元是什么?如果机器人只能前后左右走,曼哈顿距离最紧;如果能斜着走且斜向代价是 √2,对角距离最紧,展开节点最少。欧氏距离在八邻域栅格上偏保守,节点展开会多,但胜在实现简单、不容易出错。
2.3 用 Python 跑通一个可采纳启发式的 A* 最小实现
下面这段代码是我在本地验证启发式是否可采纳时最常用的骨架,地图用二维列表表示,0 可走、1 障碍。
import heapq import math def heuristic(a, b, mode="diagonal"): dx = abs(a[0] - b[0]) dy = abs(a[1] - b[1]) if mode == "manhattan": return dx + dy if mode == "euclidean": return math.hypot(dx, dy) # diagonal:八邻域,直走代价 1,斜走代价 sqrt(2) return max(dx, dy) + (math.sqrt(2) - 1) * min(dx, dy) def astar(grid, start, goal, mode="diagonal"): rows, cols = len(grid), len(grid[0]) open_heap = [(heuristic(start, goal, mode), 0.0, start)] g_score = {start: 0.0} parent = {start: None} # 八邻域方向与代价 moves = [(-1,0,1),(1,0,1),(0,-1,1),(0,1,1), (-1,-1,math.sqrt(2)),(-1,1,math.sqrt(2)), (1,-1,math.sqrt(2)),(1,1,math.sqrt(2))] while open_heap: f, g, cur = heapq.heappop(open_heap) if cur == goal: path = [] while cur: path.append(cur) cur = parent[cur] return path[::-1] if g > g_score.get(cur, float("inf")): continue for dx, dy, cost in moves: nx, ny = cur[0] + dx, cur[1] + dy if not (0 <= nx < rows and 0 <= ny < cols): continue if grid[nx][ny] == 1: continue ng = g + cost if ng < g_score.get((nx, ny), float("inf")): g_score[(nx, ny)] = ng parent[(nx, ny)] = cur heapq.heappush(open_heap, (ng + heuristic((nx, ny), goal, mode), ng, (nx, ny))) return None逻辑说明:堆里存的是 (f, g, 坐标),弹出时如果当前 g 大于已记录的最优 g,说明是过期条目,直接跳过,这是避免重复展开的关键。参数说明:mode决定启发式类型,必须和moves里的代价模型匹配;moves里斜向代价用math.sqrt(2),如果这里写成 1,对角距离启发式就不再可采纳,路径会开始绕。跑的时候先用小地图(比如 10×10)对比三种 mode 的展开节点数,能直观看到启发式紧不紧。
3. 把启发式接进真实栅格:代价地图与膨胀层
3.1 为什么原始栅格不能直接喂给启发式
真实机器人拿到的地图往往带噪声、带动态障碍,而且机器人有半径。直接把原始占据栅格丢进 A*,路径会贴着障碍边缘走,实际执行时一转弯就蹭上。常见做法是先做代价地图(costmap),把障碍按机器人内切半径做膨胀(inflation),膨胀层里设置递减的代价值。这样启发式仍然作用在栅格上,但真实代价已经包含了“离障碍太近要罚分”的信息。
Ferguson 等指南里对启发式的讨论默认代价是静态的,工程上必须补上这一层。我一般把膨胀半径设成机器人半径加 5 到 10 厘米安全余量,代价值从障碍处的 254 递减到自由空间的 0,递减函数用指数或线性都行,关键是别让膨胀层把窄通道堵死。
3.2 代价地图参数怎么设才不堵路
| 参数 | 含义 | 常用取值 | 调大后果 | 调小后果 |
|---|---|---|---|---|
| inflation_radius | 膨胀半径 | 机器人半径+0.05~0.1m | 窄通道被堵 | 贴障碍风险高 |
| cost_scaling_factor | 代价衰减速度 | 3.0~10.0 | 远离障碍仍高代价 | 膨胀层几乎无惩罚 |
| lethal_cost | 致命障碍阈值 | 253 | 几乎不产生致命格 | 噪声被当障碍 |
| neutral_cost | 自由空间代价 | 50 | 整体代价偏高 | 惩罚区分度低 |
这张表是我在多个移动机器人项目里反复调出来的经验值,具体数值要按机器人尺寸和通道宽度重算。调完一定要用同一张地图跑一遍 A*,看路径是否还穿窄通道、是否贴边。
3.3 在代价地图上验证启发式一致性的脚本片段
def check_consistency(grid, cost, heuristic_fn, moves): # 遍历所有可走格,检查 h(u) <= cost(u,v) + h(v) violations = [] rows, cols = len(grid), len(grid[0]) for x in range(rows): for y in range(cols): if grid[x][y] == 1: continue for dx, dy, step in moves: nx, ny = x + dx, y + dy if not (0 <= nx < rows and 0 <= ny < cols): continue if grid[nx][ny] == 1: continue # 真实边代价 = 移动代价 + 目标格代价地图值 edge = step + cost[nx][ny] if heuristic_fn((x, y)) > edge + heuristic_fn((nx, ny)) + 1e-9: violations.append(((x, y), (nx, ny))) return violations逻辑说明:这段脚本把一致性定义直接翻译成双重循环,返回所有违反一致性的边。参数说明:heuristic_fn是只依赖当前格到目标的函数,cost是代价地图矩阵,moves必须和 A* 里完全一致。跑出来 violations 为空,才能放心把启发式用到在线搜索;不为空就说明代价地图的惩罚项破坏了可采纳性,需要把启发式改成对代价地图取上界,或者把惩罚项从启发式里剥离。
4. 避坑与排查:启发式路径规划最常见的五类翻车
4.1 路径能走但明显绕远
现象:A* 很快返回一条路径,但人一看就知道绕了一个大圈。原因:启发式在某个区域高估了剩余代价,把更优分支剪掉了。解决:先用 3.3 的一致性检查脚本定位违反边,再把启发式换成更保守的下界,或者把代价地图的惩罚项从启发式里去掉,只保留在 g 值里。
4.2 搜索节点数突然爆炸
现象:同一张地图,改了一版代码后展开节点从几千涨到几十万。原因:启发式退化成常数 0,A* 变成 Dijkstra。解决:检查启发式函数是否被误写成返回固定值,或者坐标单位从米变成厘米导致启发式量级远小于真实代价。把启发式和真实代价打印出来对比量级,是最快的排查手段。
4.3 路径贴障碍边缘
现象:路径紧贴膨胀层外沿,执行时轻微定位误差就撞。原因:代价地图膨胀层代价值太低,或者启发式只看距离不看代价。解决:提高cost_scaling_factor,让靠近障碍的格子 g 值明显变大;同时确认 A* 在计算 g 时累加了目标格的代价地图值,而不是只加移动步长。
4.4 窄通道被判定为不可通行
现象:地图上明明有通道,A* 却返回 None。原因:膨胀半径大于通道半宽,两侧膨胀层重叠,通道被 lethal 格封死。解决:按最窄通道宽度反推最大允许膨胀半径,必要时对窄通道区域单独降低膨胀代价,或者改用更小的机器人 footprint 做规划、执行时再留安全距离。
4.5 动态障碍导致路径反复失效
现象:静态地图上跑得好,一有动态障碍就频繁重规划,甚至卡死。原因:每次重规划都从零开始搜索,启发式没有复用上一帧信息。解决:用增量搜索思路,保留上一帧的 g 值和开放列表,只更新受影响的区域;或者把动态障碍投影成短时高代价区,让启发式仍然可采纳,避免全局重算。
5. 进阶技巧:用加权 A* 换速度,以及怎么验证没换出问题
加权 A* 是我在实时性吃紧时最常用的手段:把启发式乘一个权重 w(w>1),f = g + w·h。w 越大搜索越快,但路径代价上界变成最优解的 w 倍。工程上 w 取 1.1 到 2.0 之间,多数场景能在几十毫秒内出路径,代价增加控制在可接受范围。关键是别拍脑袋定 w,要有一个验证闭环。
我一般这样做:固定同一批起点终点对,分别用 w=1.0、1.2、1.5、2.0 跑,记录三个指标——展开节点数、路径真实代价、单次规划耗时。下面这段脚本就是干这个的。
def benchmark(grid, pairs, weights): results = [] for w in weights: total_nodes, total_cost, total_time = 0, 0.0, 0.0 for start, goal in pairs: t0 = time.perf_counter() path, nodes = astar_weighted(grid, start, goal, w) total_time += time.perf_counter() - t0 total_nodes += nodes total_cost += path_cost(path, grid) results.append((w, total_nodes, total_cost, total_time)) return results逻辑说明:astar_weighted在堆排序键里用g + w * h,其余逻辑和标准 A* 一致;path_cost按真实移动代价累加,不能用 f 值代替。参数说明:weights从小到大排,pairs要覆盖开阔区、窄通道、障碍密集区三类场景,否则基准会偏乐观。跑完看哪一档的耗时满足控制周期,同时路径代价增幅还在任务允许范围内,就选那一档。
还有一个容易被忽略的点:加权 A* 返回的路径在拐角处可能更贴障碍,因为搜索被拉向目标,没时间绕开高代价区。验证时除了看代价和耗时,一定要把路径叠加到代价地图上目视检查,确认没有穿过膨胀层高风险区。我自己的习惯是每次调完 w,都拿三张典型地图各跑二十组起终点,把路径画出来扫一眼,确认没有异常贴边或穿窄通道,再上线。这个习惯帮我省过好几次后悔药。
启发式路径规划这件事,算法本身不复杂,难的是让启发式、代价模型和真实执行三者对齐。Ferguson 等那篇指南把启发式的理论边界讲得很清楚,但落地时的参数和验证闭环得自己补。希望帮到你。
本文还有配套的精品资源,点击获取