ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

A*与D*算法深度解析:从静态寻路到动态重规划的实战指南

A*与D*算法深度解析:从静态寻路到动态重规划的实战指南 1. 项目概述从寻路到决策的算法演进在机器人、游戏开发、物流规划乃至自动驾驶领域路径规划都是一个核心问题。简单来说就是给定一个起点和一个终点在充满障碍物的环境中找出一条最优或可行的移动路线。这听起来像是一个简单的几何问题但当你面对一个庞大的、动态变化的环境时它立刻变得复杂无比。A算法和D算法就是解决这类问题的两把利器它们代表了路径规划算法从静态到动态、从理论到工程实践的关键演进。我最早接触A是在十多年前做游戏AI的时候那时觉得它简直是魔法——总能找到最短路径。但后来做机器人项目当环境中的障碍物会移动时A就力不从心了每次环境一变就得从头算一遍实时性根本没法保证。直到遇到了D*才真正解决了动态环境下的实时重规划问题。今天我就结合自己这些年的踩坑经验把这两个算法的原理、区别、适用场景以及那些教科书上不会写的实操细节掰开揉碎了讲清楚。无论你是刚入门的学生还是正在为项目选型的工程师这篇文章都能给你提供直接的参考和避坑指南。2. 核心原理深度拆解启发式搜索与增量式重规划路径规划算法的核心矛盾永远是在搜索速度、路径最优性和环境适应性三者之间做权衡。A和D正是针对不同侧重点的杰出代表。2.1 A*算法静态环境下的最优路径搜索器A*算法的核心思想是启发式搜索。它并不是盲目地尝试所有可能方向而是用一个“估价函数”来智能地引导搜索方向朝着终点最有希望的区域前进。这个估价函数f(n)由两部分组成f(n) g(n) h(n)g(n)从起点到当前节点n的实际代价。这是确切的、已经发生的成本。h(n)从当前节点n到终点的预估代价。这就是“启发”部分是算法的灵魂。A*维护两个列表开放列表和关闭列表。开放列表存放待考察的节点关闭列表存放已考察完毕的节点。算法从起点开始将其加入开放列表然后循环执行以下步骤从开放列表中取出f值最小的节点即综合代价最低、最有希望的节点。将该节点移入关闭列表。考察该节点的所有相邻节点上、下、左、右、对角等取决于移动规则如果相邻节点是障碍物或在关闭列表中忽略。如果相邻节点不在开放列表中计算其g,h,f值并将其父节点设为当前节点然后加入开放列表。如果相邻节点已在开放列表中则检查通过当前节点到达它是否是一条更优路径即g值更小。如果是则更新该节点的g值和父节点。重复以上过程直到终点被加入关闭列表找到路径或开放列表为空无路径。为什么A*高效且最优关键在于启发函数h(n)。如果h(n)永远不大于从节点n到终点的真实代价那么A保证能找到最短路径。这样的h(n)被称为“可采纳的”。在网格地图中最常用的可采纳启发函数是曼哈顿距离只允许上下左右移动和对角线距离或欧几里得距离允许对角移动。h(n)越接近真实代价A搜索的效率就越高因为它能更准确地指引方向。注意h(n)绝对不能高估真实代价否则A*将失去最优性保证。这是选择启发函数时的铁律。2.2 D*算法动态未知环境下的实时规划大师D*Dynamic A*算法是为了解决A*的最大短板——静态环境假设——而生的。在真实世界中环境信息往往不完整且会动态变化机器人传感器范围有限未知区域可能突然出现障碍物已知的通道也可能被移动的物体或人挡住。D算法的核心思想是增量式重规划。它不像A那样每次变化都推倒重来而是“记住”上一次规划的结果当环境发生变化时只对受影响的部分进行局部修正从而极大地提高了重规划的效率。D*算法有两个关键概念反向搜索与A从起点向终点搜索不同D的基本版本D* Lite是从终点向起点进行反向搜索。它计算每个节点到终点的代价相当于A*中的g值但这里记为rhs值。局部一致性D*为每个节点维护两个值g(n): 当前计算的从节点n到终点的代价估计。rhs(n): 基于节点n的后继节点即下一步能到达的节点的g值计算出的一个“一步前瞻”的代价。rhs(n)的计算公式通常是rhs(n) min_{s in Succ(n)} (c(n, s) g(s))其中c(n, s)是从n到s的移动代价。如果一个节点的g(n) rhs(n)我们称该节点是局部一致的。否则它就是局部过时或局部低估的需要被重新处理。D*的工作流程可以概括为首次规划以终点为起点像A*一样反向搜索计算出所有相关节点到终点的最优代价g并生成一条从起点到终点的路径。执行与感知机器人开始沿路径移动并用传感器感知周围环境。动态重规划当机器人发现某条边的代价发生变化时例如发现新的障碍物它会更新该边连接的相关节点的rhs值将这些节点标记为“需要更新”并放入一个优先队列。然后算法从队列中取出最关键的节点进行处理通过传播代价变化修复受影响的局部区域使节点重新达到局部一致。这个过程非常高效通常只处理地图的一小部分。D与A的本质区别A是一次性的、全局的、前向的搜索D是增量的、局部的、反向的修复。D*把主要的计算量放在了首次规划上后续的调整开销很小完美适配了“已知大部分环境但会有局部意外”的真实场景。3. 算法实现的关键细节与实操要点理解了原理接下来就是动手实现。这里面的坑可比理解公式要多得多。3.1 A*实现的三大陷阱与优化技巧陷阱一数据结构的选择直接决定性能开放列表需要频繁进行“取出最小值”和“更新键值”操作。用一个简单的列表或数组每次找最小值都要遍历复杂度是O(N)地图一大就卡死。必须使用优先队列堆。在Python中heapq模块是不二之选在C中std::priority_queue是标配。import heapq open_list [] # 将节点以 (f, g, node) 元组形式放入堆中堆会根据元组第一个元素f排序 heapq.heappush(open_list, (f_score[start], g_score[start], start_node)) current_f, current_g, current_node heapq.heappop(open_list)陷阱二关闭列表的“重复访问”与“路径更新”关闭列表的目的是避免重复处理。但直接用一个集合Set来存储关闭的节点有个问题如果后来发现一条通往该节点更优的路径g值更小按照标准A*因为它在关闭列表中就会被忽略导致找不到真正的最短路径。这就是为什么在标准A描述中当开放列表中的节点有更优路径时需要更新其g值和父节点即使它“即将”被关闭或已在关闭列表中不标准A处理方式是节点一旦从开放列表弹出并处理移入关闭列表就不再考虑更新。这要求启发函数h(n)必须是一致的或称单调的即对于任意节点n及其后继节点n有h(n) c(n, n) h(n)且h(goal)0。一致的启发函数保证了一个节点第一次被从开放列表弹出时其g值就是最优的。欧几里得距离和对角线距离是一致的曼哈顿距离在允许对角移动时可能不一致需要小心。实操心得权衡“关闭”与“重开”在工程实践中对于非一致的启发函数或者为了应对一些极端情况有时会采用一种变通策略当发现一条通往关闭列表中节点的更优路径时将该节点重新放回开放列表。这会增加一些计算但能保证最优性。你需要根据地图规模、性能要求和启发函数特性来决定。陷阱三启发函数h(n)的权重调整纯粹的f g h有时搜索速度不够快。引入一个权重因子w变成f g w * h。当w 1时算法会更“贪婪”地冲向终点搜索速度极大提升但牺牲了最优性找到的是次优路径。这在游戏AI中非常常见因为玩家几乎察觉不到微小的路径长度差异但帧率提升是实实在在的。这被称为Weighted A*。3.2 D*D* Lite实现的核心优先队列与状态管理D* Lite的实现比A*复杂关键在于维护一个名为U的优先队列以及精确管理每个节点的g和rhs状态。1. 优先队列的键值设计D* Lite的优先队列排序依据是一个二维键值[k1, k2]k1 min(g(s), rhs(s)) h(s, start) kmk2 min(g(s), rhs(s))其中km是一个累加的偏移量用于处理在机器人移动过程中启发函数h的基准点变化因为h是到起点的估计而起点是机器人的当前位置会变。这个巧妙的键值设计确保了队列能按照节点修复的紧急程度正确排序。2. 节点状态机与处理逻辑每个节点都处于以下状态之一这决定了算法如何处理它局部一致g(s) rhs(s)。节点代价是最新的无需处理。局部过时g(s) rhs(s)。节点的g值过高了可能因为障碍物移除了路径变便宜了。处理方式是将其g值更新为rhs(s)并更新其前驱节点即能到达它的节点的rhs。局部低估g(s) rhs(s)。节点的g值过低了可能因为出现了新障碍物路径变贵了。处理方式是将其g值设为无穷大相当于标记为未探索然后重新计算其rhs并更新其前驱节点。3. 主循环与机器人移动的交互D* Lite的主循环是ComputeShortestPath()它不断从优先队列U中取出键值最小的节点进行处理直到队列顶部的节点键值不小于起点的键值且起点达到局部一致。机器人每移动一步或感知到代价变化就调用UpdateVertex()更新相关顶点并将其加入队列然后调用ComputeShortestPath()进行局部修复。# 伪代码框架示意 def main_loop(): Initialize() # 初始化将终点rhs设为0加入队列 ComputeShortestPath() # 首次规划 while robot.position ! goal: move_to_next_best_node() # 沿当前最优路径移动一步 scan_environment() # 感知环境 if edge_cost_changed: km heuristic(last_position, robot.position) # 更新km for affected_vertex in changed_edges: UpdateVertex(affected_vertex) # 更新顶点状态 ComputeShortestPath() # 增量重规划4. 应用场景对比与选型指南A和D没有绝对的优劣只有是否适合场景。4.1 何时选择A*算法环境完全静态且已知这是A*的主场。例如策略游戏中的地图寻路、已知布局的仓库内AGV调度、电路板布线等。计算资源有限且对路径最优性要求高A算法逻辑相对简单内存占用可控。在嵌入式设备或性能敏感的场景如果环境不变A是更轻量的选择。需要频繁进行一次性规划例如在游戏里每个单位每次移动目标都可能变化但地图本身不变。每次都用A*重新算一遍是可以接受的。A*的典型变种与应用双向A*从起点和终点同时开始搜索相遇时停止。在空旷地图上能大幅减少搜索范围。Jump Point Search专门针对网格地图的优化能跳过大量对称路径在规则网格上比传统A*快一个数量级广泛应用于RTS游戏。Any-angle A*不再限制移动方向为网格的八个方向可以寻找任意角度的平滑路径更适合机器人运动。4.2 何时选择D*算法环境部分未知或动态变化这是D*存在的根本理由。例如移动机器人探索未知环境、自动驾驶车辆在动态交通中行驶、无人机在有突发气流或移动障碍物的空域飞行。重规划频率高且要求极低的延迟机器人每秒要感知和决策很多次。如果每次变化都运行一次完整的A*CPU根本吃不消。D*的增量式更新能将重规划时间从几百毫秒降到几毫秒。起点固定目标点固定或变化缓慢D* Lite的反向搜索特性使得它在目标点固定时效率最高。如果目标点频繁变动D*的优势会减弱。D*的家族与演进原始D*由Anthony Stentz提出概念开创但实现复杂。Focused D*优化了队列排序效率更高。DLite*由Sven Koenig和Maxim Likhachev提出这是目前最流行、实现最清晰的版本。它逻辑更简洁性能与Focused D*相当论文和开源实现都很多是工程实践的首选。4.3 选型决策矩阵特性维度A* 算法D* (D* Lite) 算法选型建议环境性质完全静态、已知动态、部分未知动态选D*静态选A*规划频率一次性或低频重规划高频、实时重规划高频重规划是D*的绝对优势场景计算资源相对较低实现简单较高实现复杂内存占用大资源极度受限选A*现代处理器通常能胜任D* Lite路径最优性保证最优启发函数可采纳保证在信息已知范围内最优两者在各自条件下都能保证最优性首次规划速度快慢需要反向计算全局代价如果只规划一次A*快重规划速度慢需完全重新计算极快局部更新动态环境重规划D*碾压性优势典型应用游戏AI、静态地图导航、 puzzles移动机器人、自动驾驶、无人机根据应用领域本质需求判断个人经验之谈不要盲目追求高级算法。我曾在一个室内扫地机器人项目上一开始就用了D* Lite后来发现房间布局几乎不变最大的动态障碍物是人和宠物但移动缓慢。后来换成了A* 定期重规划的策略比如每2秒或当机器人被挡住5秒后重新运行一次A*代码复杂度直线下降稳定性和性能完全满足需求。正确的选型建立在对业务场景最朴素、最深刻的理解之上。5. 性能调优与常见问题排查算法跑起来只是第一步跑得好、跑得稳才是工程化的关键。5.1 A*性能瓶颈分析与优化地图表示优化网格粒度这是最大的权衡。网格越细路径越精细但搜索节点数呈平方增长。对于机器人通常需要比自身尺寸更细的网格例如机器人半径的1/2。对于游戏可以根据美术精度和性能决定。层次化路径规划不要在整个大地图上用细网格跑A*。先用地标或粗网格规划一条大致的路径然后在每个局部区域用细网格进行精细规划。这能指数级降低搜索空间。启发函数优化确保使用可采纳且一致的启发函数。欧几里得距离通常是最佳选择。对于允许对角移动的网格使用切比雪夫距离或对角线距离它们比欧几里得更贴近真实代价。在Weighted A*中如何选择权重w需要通过实验。可以从w1.5开始测试逐步增加监控路径长度增加百分比和搜索时间减少百分比找到一个可接受的平衡点。开放列表的优先队列实现确保优先队列的decrease-key操作高效。如果所用数据结构不支持高效的键值降低如Python的heapq一个常用技巧是当需要更新一个已在堆中节点的f值时我们不修改它而是将更新后的节点作为一个新条目再次推入堆中。由于堆顶总是最小元素旧的那个、代价更高的条目会在后面被弹出此时通过检查节点的g值是否已经更新过来判断是否忽略它。这会导致堆中有冗余条目但通常比维护一个复杂的节点索引映射更简单可靠。5.2 D* Lite实现中的坑与解决方案km值管理错误km用于修正启发值因为机器人的起点即h(s, start)中的start在移动。每次机器人移动后在感知到变化之前必须执行km h(last_position, current_position)。忘记更新km会导致优先队列排序完全错误算法无法收敛或找到错误路径。代价变化传播不完整当一条边(u, v)的代价c(u, v)改变时不能只更新u或v。根据rhs的定义rhs(u) min_{s in Succ(u)} (c(u, s) g(s))需要更新的是节点u因为它的rhs依赖于这条边。同时所有以u为后继的节点即u的前驱节点的rhs也可能因此改变也需要被标记更新。在UpdateVertex函数中更新完当前顶点后必须遍历其所有前驱顶点调用UpdateVertex更新它们。漏掉这一步会导致代价变化无法正确传播到上游节点。优先队列键值比较函数键值[k1, k2]的比较必须是字典序比较先比较k1k1小的优先级高如果k1相等再比较k2k2小的优先级高。自己实现优先队列时比较函数写错是常见bug。内存与效率D* Lite需要为每个已知节点存储g、rhs值以及前驱/后继关系。在探索范围很大的环境中内存消耗可能很大。需要实现节点的懒加载和缓存淘汰机制例如只保留机器人周围一定范围内的节点信息。5.3 通用问题排查清单问题现象可能原因A*可能原因D*排查步骤找不到路径1. 起点/终点被标记为障碍物。2. 启发函数高估破坏了可采纳性。3. 移动规则如不允许对角穿越墙角导致实际上无通路。1. 目标点被障碍物包围。2.km未更新或更新错误导致搜索方向错乱。3. 代价变化后相关节点未正确加入更新队列。1. 可视化检查起点、终点、障碍物地图。2. 检查启发函数计算。3. 单步调试观察开放列表/优先队列内容。路径明显绕远1. 启发函数权重w过大Weighted A*。2. 移动代价设置不合理如平地与爬山代价相同。1. 局部障碍物变化后重规划未覆盖受影响区域前驱节点更新遗漏。2. 传感器信息更新延迟机器人基于旧地图走到了死胡同。1. 输出路径代价g(goal)并与理想值对比。2. 检查代价变化事件的触发和处理逻辑。3. 可视化重规划过程看代价“波”是如何传播的。算法运行极慢1. 地图过大网格太细。2. 开放列表使用低效数据结构如列表。3. 启发函数h(n)0退化为Dijkstra算法。1. 环境频繁剧烈变化导致不断进行大规模重规划。2. 优先队列的键值计算或排序效率低。3. 探索范围无限扩大节点数爆炸。1. 使用性能分析工具定位热点函数。2. 考虑层次化规划或跳点搜索JPS等优化。3. 为D*设定一个最大探索半径。路径不光滑锯齿多网格算法通病移动限制在网格的8个方向。同A*是底层图表示的问题不是搜索算法的问题。后处理路径使用拉直或样条平滑算法对A*/D*生成的网格路径进行平滑处理。6. 超越A与D现代路径规划算法一览A和D是经典和基石但学术界和工业界从未停止前进。了解它们的演进能帮助我们在更复杂的场景下做出选择。RRT / RRT*快速探索随机树。特别适用于高维空间如机械臂关节空间和复杂约束下的路径规划。它不是搜索离散图而是在空间中随机采样并连接树节点。RRT能找到一条可行路径RRT* 通过后续优化能渐进逼近最优路径。在自动驾驶的复杂泊车场景中常有应用。State Lattice Planning状态栅格规划。常用于车辆、无人机等动力学模型复杂的系统。它不在位置空间搜索而是在状态空间包含位置、速度、朝向等中预先生成一组满足动力学约束的短轨迹称为“基元”然后利用A*等算法在这些基元连接成的图上搜索。这是将几何路径与运动控制结合的高级方法。Hybrid A*混合A*。自动驾驶领域规划的主流算法之一。它是对A的扩展搜索空间是连续的状态x, y, θ但使用离散的控制动作进行扩展。它考虑车辆的动力学如转弯半径因此规划出的路径本身就是车辆可执行的。相比在网格上搜索再平滑Hybrid A的结果更自然、更安全。深度学习路径规划目前更多是辅助角色。例如用神经网络预测启发函数h(n)让A*搜索更高效或者用模仿学习让智能体学习在简单环境中移动再与经典规划器结合。纯端到端的深度学习规划在安全关键领域尚不成熟但“学习搜索”的混合范式是研究热点。我的看法是不要被新名词迷惑。绝大多数工业应用尤其是对确定性和可靠性要求极高的领域A、D*及其变种仍然是中流砥柱*。新算法往往是为了解决特定领域高维、复杂约束的特定问题而生的。理解你面对的问题的本质比追逐最新的算法更重要。从A和D学起打好图搜索的基础你对任何更高级的规划算法都能更快地上手和理解。
返回列表