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

资讯详情

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

A*与D*算法深度解析:从静态寻路到动态规划的核心原理与工程实践

A*与D*算法深度解析:从静态寻路到动态规划的核心原理与工程实践 1. 从寻路到规划A与D算法的核心分野在机器人、游戏开发乃至物流调度这些领域路径规划是一个绕不开的经典问题。简单来说就是给一个智能体比如游戏里的角色、仓库里的AGV小车一张地图告诉它起点和终点让它自己找出一条最优或可行的路来。这听起来简单但地图一复杂障碍物一多问题就变得棘手。今天我想和你深入聊聊路径规划领域两座绕不开的“大山”A算法和D算法。很多资料会把它们放在一起讲但往往语焉不详让人感觉它们只是“一个静态一个动态”的区别。实际上这种理解过于表面。A和D代表了两种截然不同的规划哲学和适用场景理解它们的分野远比记住几个公式更重要。我最初接触A是在做游戏AI的时候那时觉得它简直是个“万能钥匙”直到后来做移动机器人项目在动态变化的环境中碰得头破血流才真正领略到D的精妙。所以这篇内容不只是原理复述更多是我在实际项目中踩坑、调试、优化后的一些个人解读和心得。我们会从最根本的“图搜索”思想出发拆解A为何高效再到D如何应对“计划赶不上变化”的困境最后聊聊在实际项目中如何根据需求进行选择和改造。无论你是算法初学者还是有一定经验想深化理解的开发者相信都能从中找到一些启发。2. A*算法静态世界中的最优路径探索者2.1 核心思想当“贪心”遇见“远见”A*算法的核心可以用一个非常生活化的场景来理解你要在一个陌生的城市里从火车站起点走到某个地标建筑终点。你手头有一张不完整的地图上面只标了主要道路和你的目标方向。一种最“笨”的方法是广度优先搜索BFS你像个没头苍蝇一样尝试所有从火车站出发的路口每个路口再尝试所有分支直到碰巧走到终点。这种方法保证找到最短路径但效率极低尤其是在城市很大的时候。另一种方法是深度优先搜索DFS你认准一个方向走到黑碰壁了再原路返回换条路。这种方法可能很快找到一条路但很可能绕远不是最短的。还有一种“贪心”的策略是最佳优先搜索Greedy Best-First-Search你只盯着终点的大致方向每次都选择那个看起来离终点直线距离最近的路口走。这种方法通常很快但很容易被大型建筑障碍物误导走进死胡同或者绕远路因为它完全忽略了已经走过的路程代价。A*算法的聪明之处在于它完美地融合了“脚踏实地”和“仰望星空”。它每次选择下一个要探索的节点时不仅仅看这个节点离终点还有多远“仰望星空”的启发代价还会严谨地计算从起点走到这个节点已经花了多少代价“脚踏实地”的实际代价。具体来说它用一个评估函数F(n) G(n) H(n)来决定节点的优先级G(n)从起点到当前节点n的实际代价。这是确切的、已经发生的成本比如已经行驶的距离或时间。H(n)从当前节点n到终点的预估代价。这就是“启发函数”Heuristic是算法的“嗅觉”引导搜索方向。F(n)节点的综合优先级。F值越小该节点被认为在最优路径上的可能性越大优先级越高。A*维护两个集合开放列表Open List和关闭列表Closed List。开放列表存放待考察的节点关闭列表存放已考察完毕的节点。算法从起点开始将其加入开放列表然后循环执行以下操作从开放列表中取出F值最小的节点即当前最有希望的节点作为当前节点。将当前节点移入关闭列表。遍历当前节点的所有相邻节点邻居如果邻居不可通行如障碍物或在关闭列表中则忽略。如果邻居不在开放列表中则计算其G,H,F值并将其父节点设置为当前节点然后加入开放列表。如果邻居已经在开放列表中则检查通过当前节点到达它是否是一条更优路径即G值更小。如果是则更新该邻居的G和F值并将其父节点重设为当前节点。重复上述过程直到终点被加入关闭列表找到路径或开放列表为空无路径。注意H(n)的估计必须遵守一个关键原则——可采纳性Admissibility即它永远不能高估从当前节点到终点的实际代价。常用的曼哈顿距离用于网格地图只允许上下左右移动和对角线距离切比雪夫距离用于八方向移动都满足这一条件。如果H(n)高估了A* 可能找不到最短路径如果H(n)恒为0A* 就退化成了效率低下的 Dijkstra 算法。2.2 启发函数H(n)算法的灵魂与性能关键H(n)的选择直接决定了A*算法的效率和结果。它需要在“准确性”和“计算速度”之间做权衡。曼哈顿距离H(n) |x1 - x2| |y1 - y2|。适用于网格地图且移动仅限于上下左右四方向。它简单快速是四方向移动场景下的完美启发值既不低估也不高估实际最短路径。对角线距离切比雪夫距离H(n) max(|x1 - x2|, |y1 - y2|)。适用于可以八方向移动包括对角线的网格地图。它同样满足可采纳性。欧几里得距离H(n) sqrt((x1 - x2)^2 (y1 - y2)^2)。这是两点间的直线距离。在任何方向上移动都允许时它是最自然的启发值。但需要注意在标准网格地图上如果移动成本是均匀的比如每格代价为1使用欧几里得距离会轻微高估对角线移动的实际成本实际是sqrt(2)≈ 1.414而直线距离估算是1.0到1.414之间这可能导致搜索节点增多。为了保持可采纳性有时需要对欧几里得距离进行缩放。个人心得H(n)的权重调优在有些实现中你会看到F(n) G(n) w * H(n)其中w是一个权重系数。当w 1时算法会更“贪心”更快地冲向目标搜索的节点数更少速度更快但可能牺牲最优性找到的路径可能稍长。当w 1时在启发函数可采纳的前提下保证找到最短路径。当w 1时算法更“保守”行为接近 Dijkstra会探索更多节点以确保最优但速度慢。 在实际项目中如果不是极端追求最短路径而是要求实时性如游戏AI适当调大w比如1.2到1.5可以显著提升性能且路径质量通常仍在可接受范围内。这是一个经典的“速度-精度”权衡点。2.3 实现细节与优化技巧理解了原理实现一个基础的A*并不难。但要让它在生产环境中稳定高效有几个细节必须注意开放列表的数据结构开放列表需要频繁进行“取出最小值”和“插入/更新”操作。使用一个简单的数组或列表每次查找最小值都是 O(n) 的复杂度在节点多时不可接受。优先队列二叉堆是标准选择它能让取出最小值和插入操作都在 O(log n) 内完成。在Python中heapq模块非常好用在C中std::priority_queue是标配。节点状态的维护每个节点需要记录G,H,F值以及父节点指针。此外关键是要高效判断一个节点是在开放列表还是关闭列表中。简单的做法是为节点增加一个状态标志UNVISITED,OPEN,CLOSED。更高效的做法是使用一个与地图对应的二维数组来存储节点信息通过坐标直接索引避免在列表中查找。路径回溯当算法到达终点时路径并没有显式存储。你需要从终点节点开始沿着每个节点的“父节点”指针一路回溯到起点反向得到的序列就是最终路径。地图表示与移动代价A不仅适用于网格也适用于任何图结构。图中的每个顶点是一个节点边权就是G值的一部分。你可以轻松地为不同类型的地形如草地、沼泽、公路赋予不同的移动代价A会自动计算出代价最小的路径而不一定是步数最少的路径。一个简单的网格地图APython示例核心*import heapq class Node: def __init__(self, parentNone, positionNone): self.parent parent self.position position self.g 0 # 从起点到本节点的代价 self.h 0 # 到终点的启发代价 self.f 0 # g h def __eq__(self, other): return self.position other.position def __lt__(self, other): # 用于优先队列比较 return self.f other.f def astar(maze, start, end): # 创建起点和终点节点 start_node Node(None, start) end_node Node(None, end) open_list [] closed_list set() # 使用集合提高查找效率 heapq.heappush(open_list, start_node) # 定义四方向移动向量 directions [(0, -1), (0, 1), (-1, 0), (1, 0)] while open_list: current_node heapq.heappop(open_list) closed_list.add(current_node.position) # 找到目标回溯路径 if current_node end_node: path [] current current_node while current is not None: path.append(current.position) current current.parent return path[::-1] # 返回反转的路径 # 生成子节点 children [] for new_position in directions: node_position (current_node.position[0] new_position[0], current_node.position[1] new_position[1]) # 确保在地图范围内且可通行 if (node_position[0] (len(maze) - 1) or node_position[0] 0 or node_position[1] (len(maze[0]) -1) or node_position[1] 0): continue if maze[node_position[0]][node_position[1]] ! 0: # 假设0为可通行 continue new_node Node(current_node, node_position) children.append(new_node) # 遍历子节点 for child in children: if child.position in closed_list: continue # 计算G, H, F值 child.g current_node.g 1 # 假设每步代价为1 # 使用曼哈顿距离作为启发函数 child.h abs(child.position[0] - end_node.position[0]) \ abs(child.position[1] - end_node.position[1]) child.f child.g child.h # 检查开放列表中是否已有更优的该节点 found_better False for open_node in open_list: if child open_node and child.g open_node.g: found_better True break if not found_better: heapq.heappush(open_list, child) return None # 未找到路径3. D*算法动态环境中的实时路径修复大师3.1 为什么需要D*A*的局限性现在让我们把场景从静态地图切换到动态环境。想象你正按照A规划好的最优路径走向会议室突然发现前方走廊因为临时会议被封锁了。A会怎么做它对此一无所知因为它规划时世界是静止的。你必须感知到环境变化走廊被封。以你当前所在位置为新的起点。将新发现的障碍物更新到地图中。重新运行一次完整的A*从当前位置到终点规划一条全新的路径。在机器人导航中这种“遇到障碍-完全重新规划”的模式效率很低。如果环境变化频繁机器人可能会陷入“规划-移动-遇到障碍-停下-再规划”的循环行动显得非常笨拙和卡顿。更重要的是重新规划完全抛弃了之前所有的计算成果是一种浪费。DDynamic A算法就是为了解决这个问题而生的**。它的核心思想不是“重新规划”而是“增量式修复”。当发现环境变化如出现新障碍时D*不会推倒重来而是巧妙地利用之前规划的结果只对受影响的部分路径进行局部调整和重新计算从而极大地提高了在动态环境中重新规划的效率。3.2 核心状态与反向搜索从终点出发的智慧D算法最反直觉、也最精妙的一点是它是从目标点终点开始向起点搜索的。这与A从起点向终点搜索的思路截然不同。为什么这样做这源于一个深刻的洞察在动态环境中变化障碍物的影响是局部的。当你在路径上遇到一个新障碍时受影响的只是障碍物周围的一小段路径。如果算法是从终点开始并为地图上每个节点都计算了一个“从该节点到终点的最优代价估计”那么当某个节点的代价因障碍物而改变时我们只需要传播这个代价变化给它的邻居像涟漪一样扩散直到所有受影响的节点代价更新完毕。而路径的起点机器人当前位置的代价一旦被更新新的最优路径也就随之确定了。D*为每个节点维护两个关键状态k(x)节点x的“优先级键值”。它是一个二元组[k1(x), k2(x)]用于在优先队列中排序。k1(x)可以理解为节点x的“历史最低估计代价”k2(x)是当前估计代价。算法总是优先处理k值最小的节点。h(x)节点x到目标点的当前最佳代价估计。这是算法的核心输出相当于A*中的G(n)但方向相反是到终点的代价而非从起点的代价。初始时D*以目标点为起点像Dijkstra算法一样但方向相反向整个地图传播h(x)值计算出每个节点到目标点的最优代价估计。这个过程称为“初始的完全向后搜索”。此时机器人只需要从起点开始始终选择h(x)值最小的邻居节点移动就能沿着最优路径走向终点。3.3 动态障碍处理与代价传播LPA*的桥梁原始的D*算法论文比较晦涩。在实际应用中更常见的是其优化版本DLite*它建立在另一个重要算法LPALifelong Planning A** 的思想之上理解起来更直观。LPA*/D* Lite 引入了“rhs值”的概念。对于节点xrhs(x)定义为rhs(x) min_{y in Succ(x)} (c(x, y) h(y))其中Succ(x)是x的后继节点在反向搜索中即从x能走到哪些邻居c(x, y)是从x到y的移动代价h(y)是邻居y的当前代价估计。rhs(x)可以理解为“基于邻居信息对节点x代价的一步前瞻估计”。如果h(x) rhs(x)我们说节点x是局部一致的意味着它的当前估计代价与基于其邻居的估计是一致的状态是稳定的。如果h(x) ! rhs(x)则节点x是局部不一致的意味着它的代价需要被更新。当环境发生变化时如边(u, v)的代价c(u, v)增加代表出现了障碍算法流程如下更新局部代价修改受影响边的代价c(u, v)。标记不一致重新计算节点u的rhs(u)。由于c(u, v)变了rhs(u)很可能发生变化导致h(u) ! rhs(u)节点u被标记为“不一致”并放入优先队列等待处理。传播不一致算法从优先队列中取出k值最小的不一致节点进行处理。处理的方式是尝试让节点恢复“局部一致”如果h(x) rhs(x)说明当前估计过高将其降低到rhs(x)如果h(x) rhs(x)说明当前估计过低通常是因为障碍出现导致路径变长将其设为无穷大并重新计算。无论哪种调整都会导致该节点的邻居节点可能变得不一致因此需要将这些邻居节点也加入优先队列。循环处理重复步骤3直到优先队列为空或者机器人的起点节点恢复一致。这个过程只更新了受障碍物影响区域的节点代价而不是全图。个人解读把D*想象成“拉橡皮筋”你可以把初始规划出的路径想象成一根连接起点和终点的、绷紧的橡皮筋。当路径中间出现一个障碍物时A的做法是剪断橡皮筋拿起起点那头重新扔向终点。而D的做法是用手指把障碍物处的橡皮筋轻轻“撑开”让橡皮筋绕过障碍物并自动调整整条路径的张力最终仍然形成一条绷紧的最优的新路径。这个“撑开并调整”的过程就是代价的局部传播它比“剪断重扔”要高效、平滑得多。3.4 D* Lite 伪代码与实战要点D* Lite 的伪代码比原始D*清晰很多核心是CalculateKey(s),UpdateVertex(u), 和ComputeShortestPath()三个函数。这里不展开全部代码但强调几个实战要点启发函数的运用和A一样DLite 也可以加入启发函数H(start, s)来引导搜索加速初始规划和重规划。这通过修改节点键值k的计算方式来实现。机器人的移动与规划交错在实际机器人系统中D* Lite 的运行与机器人的物理移动是并发的。一个常见的架构是规划线程不断运行ComputeShortestPath()来维护代价地图控制线程则让机器人沿着当前h(x)值下降最快的方向即梯度下降移动。当传感器发现新障碍时立即中断当前移动触发规划线程进行代价更新。处理动态障碍与移动障碍对于突然出现的静态障碍D* Lite 处理得很好。对于缓慢移动的障碍物可以将障碍物区域标记为“代价暂时极高”并设置一个衰减时间模拟障碍物离开后代价恢复的过程。对于快速移动的障碍如行人通常需要结合更快的局部避障算法如动态窗口法DWAD*负责全局路径的修正。内存与计算开销D需要存储整个地图的h,rhs,k值内存开销比A大。在非常大的地图中可以采用分层规划或滚动窗口的方法只维护机器人附近区域的精细代价地图。4. A与D的对比与选型指南理解了原理我们就能清晰地对比二者并做出正确的技术选型。特性维度A* 算法D* (D* Lite) 算法搜索方向正向搜索起点 - 终点反向搜索终点 - 起点并维护全局代价环境假设完全静态。规划前环境已知且不变。动态或部分未知。环境在规划后可能改变。规划模式一次性全局规划。给出从起点到终点的完整路径。初始规划 增量式修复。先做全局规划之后局部更新。核心响应对环境变化无感知需完全重新规划。感知变化后进行高效的局部代价传播与路径修复。计算效率单次规划效率高。在动态环境中频繁重规划总开销大。初始规划开销与A*类似。重规划效率极高尤其适合变化频繁但局部化的场景。内存开销较低。一次规划只需维护开放列表和关闭列表。较高。需要维护整个地图所有节点的状态h, rhs, k。适用场景游戏AI寻路、已知地图的物流规划、静态环境导航。机器人实时导航尤其是未知或动态环境、无人机航路动态避障、实时战略游戏单位群体寻路。选型决策树你的地图在规划完成后会变吗不会变- 优先选择A*。它更简单、高效、内存友好。会变有动态/未知障碍- 进入第2步。变化是频繁的还是偶发的偶发变化可以考虑使用A*在变化发生时重新规划。如果地图不大重规划速度快这可能是最简单的方案。频繁变化或对重规划实时性要求极高- 选择D(DLite)**。你的计算和内存资源是否充裕D需要更多内存来存储节点状态。在资源极其受限的嵌入式系统上需要谨慎评估。有时可以结合使用用A做全局粗略规划用更轻量的局部算法如向量场直方图VFH做动态避障。个人经验在室内服务机器人项目中我们最初使用A*但机器人经常在走廊遇到临时放置的椅子或行人而卡住。切换到D* Lite后机器人能够平滑地绕开障碍物并迅速回归到主干道上用户体验提升非常明显。代价是我们需要为代价地图分配一块不小的内存。5. 进阶话题与常见问题排查5.1 启发函数设计不当导致的陷阱无论是A还是D启发函数H(n)都是性能的关键。一个常见的陷阱是使用了不可采纳的启发函数。例如在网格地图中允许八方向移动却使用了欧几里得距离且未做处理这可能导致H(n)轻微高估对角线移动的成本因为实际移动成本是sqrt(2)而欧氏距离估算值小于等于这个值但若直接比较在某些格点计算中可能产生微妙的高估风险严格来说标准的欧氏距离在均匀代价网格上对于八方向移动是可采纳的但理解其与移动成本的差异很重要。更危险的是在存在地形代价差异的地图中如果H(n)忽略了地形代价可能会严重高估。排查与解决现象A*找不到最短路径或者找到的路径明显绕远。检查确保你的H(n)在任何情况下都不大于从节点n到终点的实际最小可能代价。对于网格曼哈顿距离四方向和对角线距离八方向是安全的。对于复杂地形设计一个既安全又高效的启发函数可能需要领域知识。5.2 动态障碍物处理中的“震荡”问题在D*应用于动态环境时可能会遇到“震荡”现象机器人在两个备选路径间来回切换。例如路径前方有一个缓慢移动的障碍物比如人当机器人计算出一条绕左边的路径时人又向左移动了一点导致机器人重新计算出一条绕右边的路径如此反复。排查与解决增加代价滞后不要对环境变化做出“瞬时”反应。可以设置一个阈值当障碍物持续出现一定时间后再更新地图。或者对障碍物区域的代价进行平滑处理避免突变。结合局部避障D负责全局路径的修正对于近处的、快速移动的障碍交给反应式的局部避障算法如动态窗口法DWA。D生成一条全局的“引导路径”DWA负责在跟随这条路径的同时实时避开眼前的动态障碍。路径一致性惩罚在代价函数中引入一项对路径剧烈变化的惩罚使机器人更倾向于保持原有路径方向除非新路径有显著优势。5.3 大尺度地图下的性能优化当地图非常大时如无人驾驶的高精地图无论是A的一次性搜索还是D的全局状态维护计算和内存开销都可能成为瓶颈。优化策略分层路径规划粗规划层使用一个低分辨率或拓扑结构的地图进行快速、大范围的路径规划找到从区域A到区域B的粗略路线。细规划层在粗规划指示的局部区域内使用高分辨率地图进行精细的A或D规划。这能极大减少搜索空间。路点导航预先在地图上设置关键路点Waypoints。规划时先规划起点到最近路点再规划路点到路点最后规划路点到终点。这相当于将长路径拆分成多个短路径。滚动窗口对于D*可以不维护全图代价只维护机器人周围一定半径内的窗口地图。机器人移动时窗口随之移动。这非常适合未知环境探索。5.4 非网格地图的应用A和D的本质是图搜索算法网格只是图的一种特殊形式每个格子是一个节点与相邻格子有边连接。它们可以很容易地应用于其他图结构导航网格NavMesh在游戏开发中更常用。将可行走区域划分为凸多边形。节点是多边形的中心或顶点边是多边形之间的连通关系。A*在NavMesh上运行找到的多边形序列再通过漏斗算法生成平滑的行走路径。路线网络如城市道路网交叉口是节点道路是边边权可以是距离或通行时间。A*可以用于汽车导航。状态空间规划在机械臂运动规划中节点可以是机械臂的关节角度配置边代表可行的微小运动。A*可以用于在高维状态空间中寻找无碰撞的运动路径。切换的关键在于正确定义“节点”、“邻居关系”和“边的代价”。一旦定义好这个图A和D的算法框架可以直接套用。路径规划的世界远不止A和D还有如RRT快速随机搜索树这类适用于高维空间的采样规划算法以及融合了深度学习的端到端规划方法。但A和D作为基于搜索的经典方法因其最优性、可预测性和可靠性在众多对安全性、确定性要求高的领域如机器人、工业自动化中依然占据着不可替代的位置。理解它们的原理和差异是构建更复杂智能移动系统的坚实基础。当你下次再看到游戏角色行云流水地绕过障碍或者物流小车在仓库中灵活穿梭时或许就能会心一笑知道这背后是哪些古老的智慧在默默支撑。
返回列表