ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

路径规划算法实战:Dijkstra、A*与CBS多智能体避碰详解

路径规划算法实战:Dijkstra、A*与CBS多智能体避碰详解 1. 路径规划算法的整体设计思路与选型逻辑路径规划这件事说白了就是让一个“东西”从起点走到终点中间还得绕开各种障碍。这个“东西”可以是游戏里的NPC、仓库里的AGV小车、无人机也可以是多机器人系统里的每一个独立个体。我最早接触这块是在一个仓储仿真项目里当时需要让几十台搬运机器人同时从各自的取货点走到出货口还不能撞在一起。一开始想得很简单每台车单独跑个最短路不就行了结果实测下来机器人全堵在同一个路口场面堪比早高峰。这就引出了路径规划的三个核心层次单智能体最短路、启发式加速搜索、多智能体协同避碰。Dijkstra算法解决的是第一层——在带权图上找到从起点到终点的最小代价路径它不关心效率只保证结果最优。A算法在Dijkstra的基础上引入启发函数把“盲目搜索”变成“有方向地搜索”在大多数场景下能把搜索范围缩小一个数量级。CBS算法则是面向多智能体场景的顶层框架它把多机路径规划拆解成“底层单机搜索顶层冲突消解”两个部分底层可以用A顶层用约束树来逐步消除冲突。为什么是这三个算法放在一起讲因为它们之间存在清晰的递进关系。Dijkstra是理论基础理解了它才能明白A到底“聪明”在哪里A是单机规划的主力工具实际项目中用得最多CBS则是从单机走向多机的桥梁解决的是“多个A结果放在一起会打架”的问题。我在实际项目中的选型逻辑是这样的如果地图规模小、对实时性没要求直接用Dijkstra就够了代码简单不容易出错如果地图大、需要快速响应A是首选如果场景里有多个智能体需要同时规划那就得上CBS或者它的变种。还有一个容易被忽略的点路径规划算法的选择高度依赖地图的表示方式。栅格地图、路网图、拓扑图不同的表示方式对算法效率的影响非常大。比如在栅格地图上Dijkstra的复杂度是O(V²)或者O(EVlogV)V是栅格数量一个200×200的栅格地图就有40000个节点朴素Dijkstra跑一次可能要好几秒。但如果你把地图抽象成路网图节点数可能只有几百个同样的算法瞬间就能出结果。所以我在做任何路径规划之前第一件事永远是问清楚地图怎么表示节点规模多大实时性要求是多少2. Dijkstra算法的核心原理与实操细节2.1 算法本质贪心策略下的全局最优Dijkstra算法的核心思想其实很朴素从起点开始每次选择当前距离起点最近的未访问节点然后更新它的邻居节点距离。这个过程不断重复直到所有节点都被访问或者终点被确定。它的正确性依赖于一个关键前提——所有边的权重非负。如果存在负权边贪心策略就会失效因为后面可能出现一条更短的路。我用一个生活化的类比来解释假设你在一个城市里送快递起点是仓库你要找到达每个收货点的最短路线。Dijkstra的做法是先看仓库直接相连的路口哪个最近确定这个路口的最短距离不会再变了然后以这个路口为中转站看看能不能让其他路口变得更近。一步一步往外扩直到所有路口都被确定。这个算法的时间复杂度取决于实现方式。朴素实现是O(V²)用优先队列优化后可以做到O((VE)logV)。在实际代码中我几乎不会手写朴素版本直接用优先队列最小堆实现。下面是一个Python版本的参考实现import heapq def dijkstra(graph, start, goal): # graph: dict, {node: [(neighbor, weight), ...]} dist {node: float(inf) for node in graph} dist[start] 0 prev {node: None for node in graph} pq [(0, start)] visited set() while pq: d, u heapq.heappop(pq) if u in visited: continue visited.add(u) if u goal: break for v, w in graph[u]: if v not in visited and dist[u] w dist[v]: dist[v] dist[u] w prev[v] u heapq.heappush(pq, (dist[v], v)) # 回溯路径 path [] cur goal while cur is not None: path.append(cur) cur prev[cur] return path[::-1], dist[goal]这段代码有几个细节值得注意。第一visited集合的作用是防止重复处理同一个节点因为优先队列里可能存在同一个节点的多个条目。第二if u goal: break是一个提前终止优化一旦终点被弹出它的最短距离就已经确定了不需要继续搜索。第三路径回溯用的是prev字典记录每个节点的前驱。2.2 实际项目中的性能优化技巧在实际项目中直接跑上面的代码往往会遇到性能瓶颈。我踩过的坑主要有这几个第一个坑是图的表示方式。用字典嵌套列表的方式虽然直观但在大规模图上内存开销很大。如果节点是连续的整数编号用邻接数组或者CSR格式会快很多。我在一个包含5万个节点的路网图上测试过字典方式跑一次要3秒多换成CSR格式后降到0.8秒。第二个坑是优先队列的重复入队。上面的代码里每次更新距离都会往堆里push一个新条目导致堆里可能有大量过期条目。虽然visited集合能保证每个节点只被处理一次但堆的操作次数增加了。一个优化方案是用索引堆Indexed Priority Queue支持直接修改堆中元素的值能把堆操作次数降到O(V)级别。第三个坑是提前终止的时机。有些实现是在弹出终点时终止有些是在更新终点距离时终止。前者是正确的后者不一定因为更新时只是发现了一条更短的路径但可能还有更短的路径没被发现。这个细节在面试中经常被问到。注意Dijkstra算法只适用于非负权图。如果你的场景中存在负权边比如某些代价函数可能产生负值需要改用Bellman-Ford算法或者Johnson算法。我在一个项目中遇到过代价函数设计不当导致负权的情况排查了很久才发现是算法选型的问题。3. A*算法的启发式设计与工程实践3.1 从Dijkstra到A*启发函数到底改变了什么A算法本质上就是Dijkstra加上一个启发函数h(n)。Dijkstra的优先级是g(n)即从起点到当前节点的实际代价A的优先级是f(n) g(n) h(n)即实际代价加上从当前节点到终点的估计代价。这个h(n)就像一个“指南针”告诉算法往哪个方向搜更有希望。启发函数的设计是A的核心。有一个关键性质叫可采纳性如果h(n)永远不大于从n到终点的真实最短距离那么A保证找到最优解。另一个性质叫一致性如果h(n) ≤ cost(n, n) h(n)那么A*在搜索过程中不会重复扩展节点。一致性比可采纳性更强满足一致性的启发函数一定可采纳反之不一定。在栅格地图上常用的启发函数有几种启发函数公式适用移动方式特点曼哈顿距离|x1-x2| |y1-y2|四方向移动计算快但估计偏大欧几里得距离sqrt((x1-x2)² (y1-y2)²)任意方向移动估计准确但需要开方对角线距离max(|dx|, |dy|) (sqrt(2)-1)*min(|dx|, |dy|)八方向移动兼顾准确性和速度切比雪夫距离max(|x1-x2|, |y1-y2|)八方向移动代价相同计算最快但估计偏小我一般会先用欧几里得距离如果性能不够再换曼哈顿距离。在八方向移动的场景下对角线距离是最合适的因为它精确匹配了移动代价模型。3.2 A*的代码实现与调优经验下面是一个A*的Python实现我加了详细注释import heapq import math def a_star(graph, start, goal, heuristic): # graph: dict, {node: [(neighbor, weight), ...]} # heuristic: function, h(node) - estimated cost to goal open_set [(heuristic(start, goal), 0, start)] g_score {start: 0} prev {} closed_set set() while open_set: f, g, u heapq.heappop(open_set) if u in closed_set: continue closed_set.add(u) if u goal: # 回溯路径 path [] cur goal while cur in prev: path.append(cur) cur prev[cur] path.append(start) return path[::-1], g_score[goal] for v, w in graph[u]: if v in closed_set: continue tentative_g g_score[u] w if v not in g_score or tentative_g g_score[v]: g_score[v] tentative_g prev[v] u f_score tentative_g heuristic(v, goal) heapq.heappush(open_set, (f_score, tentative_g, v)) return None, float(inf) # 无路径这段代码和Dijkstra的结构几乎一样区别就在于优先队列的排序键从g变成了f。但就是这一个改动搜索效率可能相差几十倍。我在一个100×100的栅格地图上做过对比测试Dijkstra平均扩展约8000个节点A*欧几里得启发平均扩展约600个节点速度提升超过10倍。调优经验一启发函数的权重。有时候为了进一步加速会给h(n)乘一个大于1的权重变成f(n) g(n) wh(n)。这叫加权A牺牲最优性换取速度。w越大搜索越偏向贪心速度越快但路径可能不是最优。我在实时性要求高的场景下用过w1.5路径长度平均增加3%左右但搜索时间减少40%。调优经验二打破平局。当多个节点的f值相同时优先队列的弹出顺序会影响搜索行为。一个常用技巧是在f值相同时比较h值优先扩展h更小的节点即更接近终点的节点。这能让搜索更聚焦减少扩展的节点数。调优经验三内存管理。A需要维护open_set和closed_set在大规模地图上内存开销不小。如果内存受限可以用迭代加深AIDA*它把A*的优先队列换成深度优先搜索加阈值迭代内存开销降到O(d)d是解的深度。代价是重复搜索增多时间开销变大。提示A*的启发函数必须满足可采纳性才能保证最优解。如果你不确定自己的启发函数是否可采纳一个简单的判断方法是检查h(n)是否可能大于从n到终点的真实最短距离。如果可能那就不满足可采纳性。4. CBS算法多智能体路径规划的冲突消解框架4.1 CBS的核心思想把多机问题拆成单机问题CBS的全称是Conflict-Based Search翻译过来就是“基于冲突的搜索”。它的核心洞察非常巧妙多智能体路径规划之所以难是因为智能体之间存在冲突如果能把冲突逐个消解那每个智能体就只需要解决一个带约束的单机规划问题。CBS的框架分为两层。底层是单机路径搜索可以用A*输入是地图和一组约束输出是一条满足约束的最短路径。顶层是约束树搜索每个节点代表一组约束根节点没有约束底层为每个智能体规划路径然后检测冲突。如果存在冲突就生成两个子节点分别给冲突双方添加约束禁止它们在冲突时间和位置占用该位置。这个过程不断重复直到找到一个无冲突的解。用一个类比来解释假设有两个人在走廊里迎面走来CBS的做法是先让两个人各自规划一条最短路径发现他们在某个位置会撞上。然后生成两个方案方案A让第一个人在那个时间点避开那个位置方案B让第二个人避开。分别检查两个方案如果还有冲突就继续拆分直到找到一组所有人都能走通且不碰撞的路径。4.2 CBS的实现要点与性能瓶颈CBS的伪代码逻辑不复杂但实现起来有几个关键点约束的表示。约束通常是一个三元组(agent_id, location, timestep)表示某个智能体在某个时间步不能出现在某个位置。在底层A*中需要把这些约束整合进去最简单的方式是在扩展节点时检查是否违反约束如果违反就跳过。冲突检测。需要检测两种冲突顶点冲突两个智能体在同一时间步出现在同一位置和边冲突两个智能体在同一时间步交换位置。边冲突容易被忽略但在实际场景中很常见尤其是两个智能体相向而行的时候。约束树的搜索策略。CBS的顶层可以用BFS、DFS或者最佳优先搜索。最佳优先搜索通常效率最高因为它优先扩展代价最小的节点。代价可以用所有智能体路径长度之和来衡量。下面是一个CBS的简化Python实现框架import heapq from copy import deepcopy def cbs(grid, agents): # agents: list of (start, goal) # 根节点无约束 root_constraints {i: [] for i in range(len(agents))} root_paths {} for i, (start, goal) in enumerate(agents): path a_star_with_constraints(grid, start, goal, root_constraints[i]) if path is None: return None root_paths[i] path root_cost sum(len(p) for p in root_paths.values()) open_list [(root_cost, 0, root_constraints, root_paths)] node_id 1 while open_list: cost, _, constraints, paths heapq.heappop(open_list) # 检测冲突 conflict detect_conflict(paths) if conflict is None: return paths # 找到无冲突解 agent_i, agent_j, loc, time conflict # 生成两个子节点 for agent in [agent_i, agent_j]: new_constraints deepcopy(constraints) new_constraints[agent].append((loc, time)) new_paths deepcopy(paths) new_path a_star_with_constraints( grid, agents[agent][0], agents[agent][1], new_constraints[agent] ) if new_path is None: continue new_paths[agent] new_path new_cost sum(len(p) for p in new_paths.values()) heapq.heappush(open_list, (new_cost, node_id, new_constraints, new_paths)) node_id 1 return None # 无解CBS的性能瓶颈主要在底层A的调用次数。每处理一个冲突就要重新规划两条路径如果冲突很多调用次数会急剧增加。我在一个20个智能体的场景中测试过冲突数量可能达到上百个底层A调用上千次规划时间从单机的毫秒级变成秒级。优化方向一冲突选择策略。不是所有冲突都同等重要优先处理“最严重”的冲突比如涉及智能体数量多、时间早的冲突能减少约束树的规模。常用的策略有“最早冲突优先”和“最多冲突优先”。优化方向二约束传播。当一个智能体被添加约束后可能会产生新的冲突可以提前检测并处理避免无效的搜索分支。优化方向三使用更快的底层规划器。底层A可以用JPSJump Point Search加速在栅格地图上能进一步减少扩展节点数。或者用DLite做增量搜索当约束变化时只更新受影响的部分。注意CBS保证找到最优解但前提是底层规划器也保证最优。如果底层用了加权A*那CBS的整体最优性就不成立了。在实际项目中如果对最优性要求不高可以用加权A*加速底层搜索整体解的质量通常也够用。5. 三种算法的对比与选型指南5.1 性能对比与适用场景把三种算法放在一起对比能更清楚地看出各自的定位维度DijkstraA*CBS问题类型单机最短路单机最短路多机协同规划最优性保证最优启发函数可采纳时最优底层最优时保证最优时间复杂度O((VE)logV)取决于启发函数质量指数级最坏情况空间复杂度O(V)O(V)O(V * 智能体数 * 约束数)适用规模小规模图中大规模图小规模多智能体实现难度低中高实时性差好差从表中可以看出Dijkstra适合作为教学和基准实际项目中用得越来越少。A*是单机规划的主力几乎能覆盖大部分单机场景。CBS适合智能体数量不多通常10个以内且对最优性有要求的场景。如果智能体数量多、实时性要求高CBS可能不是最佳选择需要考虑基于规则的方法或者学习类方法。5.2 实际项目中的组合使用策略在实际项目中我很少只用一种算法更多是根据场景组合使用。举几个例子仓储机器人场景地图是栅格化的仓库布局智能体是几十台AGV。单机路径用A*但为了避免拥堵会在代价函数中加入“拥堵惩罚”——某个位置被占用的次数越多通过代价越高。多机协同不用完整的CBS而是用“优先级规划”给每台AGV分配优先级高优先级的先规划低优先级的在规划时把高优先级的路径当作动态障碍。这种方法不保证最优但速度快适合实时调度。游戏NPC寻路地图通常是导航网格NavMesh节点数不多。单机用A*启发函数用欧几里得距离。多机场景下NPC之间的避碰更多依赖局部避障算法如RVO而不是全局的CBS。因为游戏对实时性要求极高CBS的指数级复杂度不可接受。无人机编队三维空间智能体数量通常不超过10个。单机用A的变种如Theta支持任意角度移动多机用CBS或者其优化版本如ECBS允许次优解但速度更快。三维空间的冲突检测比二维复杂需要考虑高度层的分配。提示算法选型没有银弹。我在项目中总结的原则是先确定问题规模节点数、智能体数再确定实时性要求毫秒级、秒级、分钟级最后确定最优性要求必须最优、可以次优。这三个维度确定后算法选择基本就明确了。6. 常见问题与排查技巧实录6.1 路径规划中的典型问题速查在实际开发和调试中我遇到过各种各样的问题。下面整理了一个速查表覆盖了最常见的几类问题现象可能原因排查方法解决方案A*找不到路径启发函数不可采纳检查h(n)是否可能大于真实距离换用可采纳的启发函数A*路径不是最优启发函数权重过大对比w1和w1.5的路径长度减小权重或改用标准A*Dijkstra运行超时图规模太大统计节点数和边数改用A*或抽象地图CBS无解约束冲突导致底层无路径检查约束是否矛盾放宽约束或增加智能体等待时间CBS运行超时冲突太多统计约束树节点数用ECBS或优先级规划路径贴障碍物太近代价函数未考虑安全距离检查障碍物周围的代价增加膨胀层或安全代价多机路径抖动冲突消解策略不稳定观察路径变化频率加入路径平滑或滞后机制6.2 独家避坑经验分享坑一栅格地图的对角线移动。在八方向移动的栅格地图上如果允许对角线移动需要检查对角线方向的两个相邻格子是否都是可通行的。否则会出现“穿墙”现象——智能体从两个障碍物的对角缝隙中穿过去。这个bug很隐蔽因为路径看起来是合法的但实际物理上不可行。坑二A*的启发函数与移动模型不匹配。如果移动模型是八方向但启发函数用曼哈顿距离h(n)会高估真实距离导致A*不保证最优。我见过一个项目用曼哈顿距离做八方向移动的启发路径长度比最优解多了15%。换成对角线距离后问题解决。坑三CBS的约束累积。CBS在生成子节点时约束是累积的。如果实现时不小心覆盖了父节点的约束而不是追加会导致搜索不完整可能错过最优解。这个bug在代码审查时很难发现因为约束树看起来是正常的但解的质量会下降。坑四时间步的粒度。CBS中的时间步通常与路径的步数对应。如果智能体可以等待需要在路径中插入等待动作。等待动作的表示方式会影响冲突检测和约束生成。我建议把等待显式表示为一个“停留在原地”的动作这样冲突检测逻辑更统一。坑五浮点数精度。如果代价是浮点数比较时要用一个小的epsilon避免因为精度误差导致路径选择不稳定。我在一个项目中用欧几里得距离作为代价结果因为浮点数比较问题同样的输入有时得到不同的路径。后来统一用整数代价距离乘以1000取整才解决。坑六内存泄漏。CBS的约束树可能非常大如果不及时释放不再需要的节点内存会迅速增长。在Python中用deepcopy复制约束和路径时尤其要注意深拷贝的开销很大。一个优化是用不可变数据结构如frozenset来表示约束避免深拷贝。6.3 调试工具与技巧调试路径规划算法光看代码是不够的可视化工具必不可少。我常用的方案是用matplotlib实时绘制搜索过程和最终路径。对于栅格地图用imshow显示地图用不同颜色标记open_set、closed_set和最终路径。对于CBS可以画出约束树的结构每个节点显示约束和当前代价。另一个技巧是记录搜索日志。在A*中记录每次扩展的节点和f值事后分析搜索行为。如果发现搜索范围异常大可能是启发函数的问题如果发现路径异常可能是代价函数的问题。还有一个实用的技巧是构造测试用例。我通常会准备几组标准测试空地图、有障碍物的地图、迷宫地图、多智能体冲突场景。每次修改算法后跑一遍测试确保没有回归。测试用例的输入和期望输出都保存下来方便对比。7. 从单机到多机的扩展思路7.1 多智能体场景的额外挑战从单机路径规划扩展到多机不只是“多跑几次A*”那么简单。多机场景引入了几个单机没有的挑战冲突不可避免。在共享空间中智能体之间的冲突是常态而非例外。冲突检测和消解是多机规划的核心。解的质量衡量标准变了。单机看路径长度多机要看所有智能体路径长度之和、最大完成时间、总等待时间等多个指标。不同指标对应不同的优化目标。计算复杂度爆炸。单机A*是多项式复杂度多机CBS在最坏情况下是指数复杂度。智能体数量增加时规划时间可能急剧增长。实时性要求更高。多机场景通常需要在线规划因为环境和其他智能体的状态在不断变化。离线规划一次然后执行的方式往往不可行。7.2 实用扩展方案针对这些挑战我在项目中用过几种实用的扩展方案优先级规划是最简单的多机方案。给每个智能体分配优先级按优先级顺序依次规划低优先级智能体把高优先级智能体的路径当作动态障碍。优点是实现简单、速度快缺点是不保证最优且优先级分配对结果影响大。我通常用“路径越长优先级越高”或者“起点越靠前优先级越高”的策略。迭代修复是另一种思路。先让所有智能体独立规划然后检测冲突对冲突的智能体重新规划加入避让约束重复直到无冲突或达到最大迭代次数。这种方法比CBS简单但不保证收敛。分层规划把问题分成全局和局部两层。全局层用CBS或优先级规划生成粗略路径局部层用避障算法如RVO、ORCA处理实时避碰。这种方案在游戏和仿真中很常见兼顾了全局最优性和局部实时性。时间窗规划适合有固定时间表的场景。每个智能体有一个时间窗规划时不仅考虑空间避让还考虑时间避让。这种方法在交通调度中很常用。提示多机路径规划没有“万能方案”。我在项目中选方案时会先问三个问题智能体数量多少实时性要求多高对最优性要求多严答案不同方案完全不同。8. 算法实现中的工程化考量8.1 代码结构与模块化路径规划算法的代码如果写得不好很容易变成一团乱麻。我在项目中总结的模块化方案是这样的地图模块负责地图的加载、表示和查询。提供is_free(node)、get_neighbors(node)、get_cost(node1, node2)等接口。地图的表示方式栅格、路网、拓扑对上层透明。规划器模块实现Dijkstra、A*、CBS等算法。每个算法是一个类提供plan(start, goal)或plan(agents)接口。规划器依赖地图模块但不依赖具体的应用场景。冲突检测模块负责检测多机路径之间的冲突。提供detect_conflict(paths)接口返回冲突列表。冲突检测逻辑独立于规划算法方便替换和测试。可视化模块负责绘制地图、路径和搜索过程。调试时用生产环境可以去掉。这种模块化设计的好处是换地图表示方式不影响规划算法换规划算法不影响冲突检测各模块可以独立测试和优化。8.2 性能优化的工程手段除了算法层面的优化工程层面也有很多手段可以提升性能缓存如果多次规划请求的起点终点相同可以缓存结果。在仓储场景中取货点和出货点通常是固定的缓存命中率很高。并行化CBS的底层A*调用可以并行化因为不同智能体的规划是独立的。用多线程或多进程加速在智能体数量多时效果明显。增量更新如果地图只有局部变化不需要重新规划整条路径。D* Lite等增量算法可以只更新受影响的部分大幅减少计算量。预计算对于静态地图可以预计算所有节点对之间的最短距离存成查找表。规划时直接查表O(1)时间。缺点是内存开销大适合节点数不多的场景。降采样如果地图分辨率很高可以先降采样成低分辨率地图做粗略规划再在高分辨率地图上做局部细化。这种方法在图像处理中很常见路径规划也可以借鉴。8.3 测试与验证路径规划算法的测试不能只看“能不能跑通”还要验证解的质量和稳定性。我通常从几个维度测试正确性测试构造已知最优解的地图验证算法输出是否匹配。比如空地图上从(0,0)到(10,10)最优路径长度是10四方向或10*sqrt(2)八方向。性能测试在不同规模的地图上测试运行时间画出时间随节点数变化的曲线。确认复杂度符合预期。稳定性测试同样的输入跑多次确认输出一致。如果输出不稳定检查是否有随机因素或浮点数精度问题。边界测试起点等于终点、起点或终点在障碍物上、地图全空、地图全堵、无解场景。这些边界情况最容易出bug。压力测试大规模地图、大量智能体、长时间运行。观察内存和CPU使用情况确认没有泄漏或性能退化。9. 实际项目案例拆解9.1 仓储多机器人调度系统这是我做过的一个比较完整的项目。场景是一个2000平米的仓库有50台AGV需要从各自的取货点走到出货口。地图是栅格化的每个格子0.5米总共约8000个格子。方案设计单机路径用A*启发函数用对角线距离。多机协同用优先级规划加局部避障。优先级按“任务紧急程度”分配紧急任务优先规划。局部避障用时间窗方法每台AGV在规划时检查未来一段时间内是否有其他AGV占用目标位置。遇到的问题最初用完整的CBS50台AGV的规划时间超过10秒完全无法接受。后来改成优先级规划规划时间降到200毫秒以内。代价是路径总长度增加了约8%但在可接受范围内。优化过程A*的启发函数从欧几里得距离换成对角线距离后搜索节点数减少约30%。加入路径缓存后重复任务的规划时间降到接近零。用CSR格式存储地图后内存占用减少60%。9.2 游戏NPC寻路系统另一个项目是游戏中的NPC寻路。地图是导航网格约500个多边形。NPC数量动态变化最多同时有30个。方案设计单机用A*在导航网格上搜索。启发函数用欧几里得距离。多机不用全局规划而是用局部避障RVO处理NPC之间的避碰。全局路径只规划一次局部避障实时调整。遇到的问题NPC在狭窄通道中容易堵死。原因是RVO的避障半径设置过大导致NPC互相推挤。后来根据通道宽度动态调整避障半径问题解决。优化过程A*的搜索用二叉堆实现比Python内置的heapq快约20%。路径平滑用拉绳算法string pulling去掉不必要的拐点让NPC移动更自然。9.3 无人机编队路径规划第三个项目是无人机编队5架无人机在三维空间中从起点飞到终点保持编队形状。方案设计单机用Theta*A*的变种支持任意角度移动多机用CBS。三维空间的冲突检测考虑水平距离和垂直距离。遇到的问题CBS的约束树增长太快5架无人机的规划时间有时超过5秒。后来用ECBS允许次优解把规划时间降到1秒以内路径长度增加约5%。优化过程底层Theta*的启发函数用三维欧几里得距离。冲突检测用空间哈希加速把O(n²)的检测降到接近O(n)。10. 算法学习与进阶建议10.1 学习路径如果你刚接触路径规划我建议的学习顺序是这样的先理解Dijkstra手动模拟几遍算法过程确保明白为什么贪心策略能得到全局最优。然后学A*重点理解启发函数的作用和可采纳性的含义。接着学CBS理解约束树和冲突消解的逻辑。最后根据项目需求学习特定场景的优化算法如JPS、D* Lite、ECBS等。实践方面我建议从简单的栅格地图开始自己实现一遍Dijkstra和A*用可视化工具观察搜索过程。然后尝试多智能体场景实现一个简化版的CBS。最后找一个实际场景比如游戏寻路、机器人仿真把算法应用上去。10.2 进阶方向路径规划是一个活跃的研究领域有很多进阶方向可以探索基于采样的规划RRT、RRT*、PRM等算法适合高维空间和复杂约束的场景。它们不依赖于地图的显式表示而是通过随机采样探索空间。基于学习的规划用神经网络学习启发函数或直接学习规划策略。这类方法在特定场景下可以比传统算法快很多但泛化能力是挑战。动态环境规划D* Lite、LPA*等算法适合环境动态变化的场景。它们支持增量更新不需要从头重新规划。多目标规划同时优化多个目标如路径长度、安全性、平滑度。这类问题通常用多目标优化算法求解。大规模多机规划当智能体数量达到数百甚至数千时CBS不再适用。需要研究基于规则、基于市场或基于学习的方法。提示路径规划算法的选择没有绝对的对错只有适合不适合。我在项目中见过用Dijkstra解决所有问题的也见过用强化学习做路径规划的。关键是理解场景需求选择最合适的工具。11. 写在最后的一些个人体会路径规划这个领域入门容易精通难。Dijkstra和A*的代码可能半天就能写出来但要在实际项目中用好需要大量的调试和优化经验。我踩过的坑包括但不限于启发函数设计不当导致路径次优、约束累积逻辑错误导致CBS无解、浮点数精度问题导致路径抖动、内存泄漏导致长时间运行崩溃。有一个体会特别深算法的理论复杂度和实际性能往往不是一回事。教科书上说A的时间复杂度是O(b^d)但实际性能取决于启发函数的质量、地图的结构、优先队列的实现方式。我在一个项目中用同样的A算法换了一种地图表示方式后性能提升了5倍。所以不要迷信理论分析实测才是硬道理。另一个体会是多机路径规划的难点不在算法而在问题建模。如何定义冲突、如何分配优先级、如何衡量解的质量这些建模决策对最终效果的影响往往比算法本身更大。我在项目中花在建模上的时间通常比写算法的时间多得多。最后路径规划是一个实践性很强的领域。看再多的论文和教程不如自己动手实现一遍。从简单的栅格地图开始逐步增加复杂度遇到问题就调试和优化。这个过程虽然辛苦但收获是实实在在的。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进