54 KiB
性能优化C# A*寻路算法:GitHub上的高性能实现深度解析
I. 执行摘要
A*算法作为路径规划领域的基石,在游戏开发、机器人导航、物流优化和人工智能等多个领域发挥着举足轻重的作用。它以其在静态环境中寻找最优路径的能力而备受青睐,在完备性和计算效率之间取得了平衡。当需要在已知且不变的图或网格中找到两点之间的最短或最低成本路径时,A*通常是首选算法。
然而,尽管A*算法在理论上表现出色,但其在C#中的标准实现常常面临显著的性能瓶颈。这些瓶颈通常源于低效的数据结构,尤其是“开放列表”(优先级队列),导致成本高昂的插入和提取操作。此外,堆上过多的对象分配会增加垃圾回收的开销,从而导致性能出现不可预测的波动。次优的网格遍历技术和冗余计算进一步加剧了这些问题,使得基本实现不足以应对大规模或实时应用的需求 1。
要在C#中实现高性能A*,需要采取多方面的方法。关键策略包括:采用高效的优先级队列(例如二叉堆)、利用C#的值类型(struct)来表示节点以减少垃圾回收开销、以及实现专门的网格表示(例如直接的“计算网格”、线性数组和2的幂次方网格尺寸以实现位运算)。算法层面的改进,例如“忽略旧节点”技术和优化的网格清理机制,在最大限度地减少冗余工作和提高吞吐量方面也发挥着至关重要的作用 1。
开源社区提供了这些优化实践的优秀范例。例如,roy-t/AStar项目展示了针对网格和图的现代高性能实现,而CastorTiu在CodeProject上发表的“Fast PathFinder”文章中概述的详细原理,则为理解各种底层优化如何产生复合效应提供了宝贵的见解。BlueRaja/High-Speed-Priority-Queue-for-C-Sharp存储库虽然并非完整的A*实现,但它是任何C# A*解决方案实现峰值性能的关键基础组件 1。
最终,C#中A*算法的最佳性能是一个全面的工程挑战,需要仔细考虑数据结构效率、内存管理和算法的独创性。此外,路径规划算法本身的选择(例如A*与D* Lite等动态变体)至关重要,必须与环境特性(尤其是其动态性)相符。开发人员必须严格测试其解决方案,以验证其在特定应用场景中的性能提升。
II. A*寻路基础
A*算法解析
A*是一种启发式搜索算法,旨在加权图或网格中查找从指定起始节点到目标节点的最短路径。其“启发式”特性源于它使用启发式函数来指导搜索,使其比非启发式算法更高效。它通过维护两个列表来运行:一个“开放列表”(待评估节点)和一个“关闭列表”(已评估节点) 7。
- 核心原理
A*算法的核心在于其对每个节点的评估,该评估结合了从起始点到当前节点的实际成本和从当前节点到目标点的估计成本。这种结合使得A*能够在探索最有可能通向目标的路径时,同时避免不必要的搜索。算法在每次迭代中都会从开放列表中选择估计总成本最低的节点进行扩展,从而确保在满足启发式函数条件下找到最优路径。 - 组成部分
- 节点与边
节点是搜索空间的基本构建块,代表离散位置,例如网格单元格或交叉点。边表示节点之间的连接,通常与“成本”或“权重”相关联。这些成本可以代表距离、时间、资源消耗等 7。 - 成本函数(g(n))
这个值代表从起始节点到当前节点n的实际累积路径成本。当从当前节点移动到下一个相邻节点时,新成本的计算方式为:newCost = costSoFar[current] + graph.Cost(current, next)。costSoFar字典存储了到达每个节点迄今为止的最低累积成本 7。 - 启发式函数(h(n))
这是从当前节点n到目标节点的估计成本。启发式函数对于A*的效率至关重要;一个好的启发式函数可以显著减少探索的节点数量。对于基于网格的寻路,常见的启发式函数包括曼哈顿距离(适用于4方向移动:∣a.x−b.x∣+∣a.y−b.y∣)和欧几里得距离(适用于8方向或连续移动)。为了保证A*找到最优路径,启发式函数必须是可接受的(从不高估到目标的真实成本)和一致的(从n到目标的估计成本小于或等于移动到相邻节点$n'的成本加上从n'$到目标的估计成本) [7, 7。 - 评估函数(f(n))
这是A*优先级排序的核心,计算公式为:f(n)=g(n)+h(n)。这个值代表从起始节点经过当前节点n到目标的估计总成本。$f(n)$值越低的节点被认为越有希望,并被优先扩展 8。 - 开放列表(前沿/优先级队列)
这是一个数据结构,用于存储所有已发现但尚未完全评估的节点。节点根据其$f(n)值进行排序,其中f(n)$值最低的节点具有最高优先级。算法不断从该列表中提取最高优先级的节点进行处理 7。 - 关闭列表(cameFrom/costSoFar)
这些通常是Dictionary对象。cameFrom存储每个已访问节点的父节点,以便在找到目标后重建路径。costSoFar存储从起始点到每个节点迄今为止找到的最低累积成本。这些列表可防止算法不必要地重新访问和重新评估节点,从而避免无限循环或低效搜索 7。
- 节点与边
标准C#实现基线 (Red Blob Games)
Red Blob Games 提供的C#实现 7 是一个出色的A*算法基本教学示例。它清晰地定义了
AStarSearch方法,接受Graph、start Location和goal Location作为参数。它使用Dictionary<Location, Location>来存储cameFrom路径信息,并使用Dictionary<Location, double>来跟踪costSoFar。Location被定义为一个包含整数x和y坐标的struct,并且为了在Dictionary和HashSet等基于哈希的集合中正确高效地使用Location对象作为键,它重写了Equals和GetHashCode方法 7。
一个关键的观察点是Red Blob Games实现的PriorityQueue类。作者明确指出,这是一个“占位符,效率低下的实现”,它使用了List的Tuple<TElement, TPriority> 7。这种简单的基于
List的方法需要线性扫描来查找和移除最高优先级的元素,使得Dequeue操作在最坏情况下具有$O(N)$的时间复杂度,其中$N$是队列中的元素数量。
基于List的优先级队列虽然易于理解,但代表着一个显著的性能瓶颈。对于大型图,在每次迭代中频繁地对前沿队列进行$O(N)$的入队和出队操作,会占据算法总执行时间的大部分,使得算法的运行速度慢得令人无法接受。Red Blob Games 自己也建议使用C# 2020+中内置的PriorityQueue<>或其它高速优先级队列库来获得生产级别的性能 1。
A*算法的核心循环会重复地从“开放列表”中提取$f(n)$值最低的节点,并插入新的或更新的相邻节点。如果“开放列表”使用简单的`List`实现(如Red Blob Games基线所示 7),查找最小元素需要遍历所有$N$个元素,时间复杂度为O(N)。类似地,在维护排序顺序(如果尝试的话)或移除任意元素时,也可能是O(N)。由于这些操作在主while循环中频繁发生(对于V个节点,大约运行V次;对于E条边,大约运行E次),因此整体复杂度会迅速升级到$O(V^2)$或$O(VE)$,从而使该算法对于大型搜索空间来说变得不切实际。Red Blob Games在7中明确警告其基于
List的PriorityQueue效率低下,并建议使用优化的替代方案,这直接证实了这是一个主要的性能瓶颈。这一观察强调了算法设计和优化中的一个基本原则:关键操作的数据结构选择通常比任何其他因素更能决定算法的整体性能特征。一个看似小的$O(N)$操作,当在嵌套循环中重复执行时,可以将一个原本高效的算法转变为性能瓶颈。这为A*中高效优先级队列的关键作用奠定了基础。
A*与其他寻路算法的比较
- Dijkstra算法
A*本质上是Dijkstra算法的改进。Dijkstra算法查找图中从单个源节点到所有其他可达节点的最短路径。相比之下,A*通过使用启发式函数来指导搜索,专门针对查找单个特定目的地的最短路径进行了优化。对于单目的地寻路,A*通常比Dijkstra算法探索的节点少得多,从而显著加快了计算时间,尤其是在大型地图上。例如,一项基准测试显示,在包含5000个节点的地图上,A*比Dijkstra算法快约7倍,同时仍能找到相同的最优路径 [12, 7。 - 广度优先搜索 (BFS)
BFS通过在当前深度级别探索所有相邻节点,然后移动到下一级别,从而在无权重图中找到最短路径。虽然它在无权重场景中保证了最短路径,但对于加权图或包含障碍物的复杂环境,它变得非常低效,因为它不根据成本优先考虑路径。A*及其成本函数和启发式函数专为加权图设计,在此类上下文中效率更高 [75, S_R42, S_S8]。 - 深度优先搜索 (DFS)
DFS通过尽可能深入地遍历每个分支,然后回溯来探索图。它不保证找到最短路径,因此不适用于大多数需要最优性的寻路应用。
A*的核心区别在于其“启发式”特性,这意味着它使用启发式函数(h(n))来估计到目标的成本 [7。这种指导允许A*优先探索似乎直接通向目标的路径,从而有效地修剪掉搜索空间中不太可能包含最优路径的大部分。这与Dijkstra算法(向所有方向扩展直到找到目标)或BFS(逐层探索而不考虑路径成本)等“非启发式”算法形成鲜明对比。12中的定量数据表明A*在5000节点地图上比Dijkstra“快约7倍”,这直接证明了这种性能优势。启发式函数减少访问节点数量的能力(如12中的视觉比较所示)是这种加速的直接原因。这一观察强调了算法选择是一个关键的性能决策。对于需要在加权图中找到单目的地最短路径的问题,A*通常是更好的选择,因为它具有智能剪枝功能。然而,A*的有效性高度依赖于其启发式函数的质量和可接受性。选择不当的启发式函数可能导致次优路径,甚至降低性能,有时甚至比更简单的非启发式搜索更慢。这突出了在设计有效启发式函数时领域特定知识的重要性。
III. 性能优化的必要性
为何优化A*?
- 大型搜索空间
现代应用程序,尤其是在游戏开发(例如开放世界环境、大型策略地图)、机器人导航和复杂网络路由中,涉及包含数百万个节点的地图或图。未经优化的A*算法可能需要数秒甚至数分钟才能计算出路径,使其无法使用 1。 - 实时性要求
许多应用程序要求即时寻路。游戏、自动驾驶车辆和实时策略 (RTS) 模拟需要路径在毫秒内计算完成,以确保流畅的游戏体验、响应式导航或即时决策。延迟可能导致糟糕的用户体验或关键系统故障 1。 - 高智能体密度
在涉及大量智能体(例如人群模拟、多机器人协调)的场景中,每个智能体都需要频繁地重新计算路径,即使是中等效率的算法,其累积计算负载也可能变得不堪重负。这会导致系统变慢、帧率下降或智能体“卡住”。 - 动态环境(即使有重新规划)
尽管A*主要用于静态环境,但即使在半动态场景中,由于微小变化或新信息需要重新计算路径时,底层A*实现的效率也至关重要。频繁的重新规划会放大核心算法中的任何低效率。
识别性能瓶颈
- 低效数据结构操作
A*中最常见且最重要的瓶颈是其核心数据结构(特别是“开放列表”(优先级队列)和“关闭列表”)的性能。如果这些数据结构使用List或ArrayList等通用集合实现,则添加、删除或搜索元素等操作可能具有$O(N)$时间复杂度。CodeProject文章明确指出,“搜索开放和关闭节点列表所花费的时间”是标准A*实现中的“主要瓶颈” 1。 - 过多内存分配(垃圾回收开销)
在C#和其他托管语言中,在堆上频繁创建对象(例如,为每个节点或路径段使用class实例)会导致垃圾回收器(GC)的压力增加。GC周期可能在实时应用程序中引入不可预测的暂停或“卡顿”,严重影响响应能力 1。 - 冗余计算
重复计算相同的值(例如成本或启发式估计),或在紧密循环中执行复杂的坐标转换,可能会累积显著的开销。尽管这些操作单独来看很小,但当执行数百万次时,它们可能会成为主要的性能消耗 1。
CodeProject文章 1 提供了一个引人注目的例子,说明了同时优化CPU周期和内存占用对性能的重要性。文章指出,主要瓶颈是“搜索开放和关闭节点列表所花费的时间”,这表明与低效数据结构访问相关的CPU密集型问题。同时,它强调将节点改为
struct“减少了垃圾回收开销”,解决了与内存相关的性能问题。文章中的“Fast PathFinder”实现了“300到1500倍”的速度提升,但代价是“对于1024x1024的网格,额外增加了13MB的内存”。这明确展示了经典的空时权衡:投入更多内存(用于直接访问的“计算网格”和可能更大的优先级队列结构)可以显著减少查找和操作所需的CPU周期,从而带来整体性能的提升。这一分析强调,性能优化很少是一蹴而就的。它通常是一个整体过程,其中一个领域的改进(例如,数据结构效率降低CPU周期)可能需要在另一个领域进行权衡(例如,增加内存使用)。深入理解所选编程语言(本例中为C#)如何管理内存(堆与栈、垃圾回收)与理解算法复杂度同样重要。开发人员必须仔细分析其应用程序的具体限制(例如,嵌入式设备上的内存限制与游戏PC上充足的RAM)以做出明智的架构决策。
优化格局
- 速度与内存
这是一个反复出现的主题。实现更高的速度通常需要使用更多的内存,例如通过预计算数据结构、更大的查找表或更复杂的优先级队列数据结构。CodeProject的优化就明确指出了其内存开销 1。 - 路径质量与速度
激进的启发式算法、简化假设(例如,仅使用整数成本)或某些算法捷径可以加速寻路,但可能导致路径并非真正最优(最短或成本最低)。CodeProject实现中的“重新开放关闭节点”设置就是一个很好的例子:启用它会产生“更好、更平滑的路径”,但“会花费更多时间”。开发人员必须决定严格的最优性是否比实时响应性更优先1, 7。 - 实现复杂性
高度优化的算法,特别是那些采用巧妙位运算、自定义数据结构或复杂内存管理的算法,在实现、调试和维护方面固有地比简单、教科书式的版本更复杂。这需要在开发工作量和运行时性能之间进行权衡 1。
IV. C# A*核心优化策略
提升效率的高级数据结构
- 优先级队列
- 基石
高效的优先级队列可以说是A*性能最关键的组成部分。它管理着“开放列表”,确保始终首先检索到$f(n)$值最低(估计总成本)的节点进行扩展。这个操作,即Dequeue(或extract-min),以及Enqueue(插入新节点)和可能的Decrease-Key(更新节点优先级),在算法执行过程中会重复进行 7。 - 优化实现
- 二叉堆 (MinHeap)
这是A*优先级队列最常见且广泛推荐的数据结构。它为Enqueue(插入)和Dequeue(提取最小元素)操作提供了对数时间复杂度(O(logN)),其中N是堆中的元素数量。这使其比简单的基于List的方法性能显著提高 3。
BlueRaja/High-Speed-Priority-Queue-for-C-Sharp库是专为C#寻路优化的二叉堆的典型示例,强调速度和低开销 4。
roy-t/AStar库也明确利用了MinHeap来实现其高性能 3。 - 斐波那契堆
虽然理论上为decrease-key操作提供了卓越的渐近复杂度($O(1)$摊还),但斐波那契堆通常具有更高的常数因子,并且实现起来更复杂。在实践中,对于大多数A*场景,二叉堆由于结构更简单且缓存性能更好,通常会优于斐波那那契堆 5。 - SortedSetPriorityQueue (红黑树)
使用System.Collections.Generic.SortedSet(通常实现为红黑树)可以为插入、decrease-key和提取最小元素操作提供$O(\log V)$的复杂度。虽然这是一个可行的选择,但自定义的二叉堆实现通常针对寻路特定需求进行了优化,并能提供更好的实际性能 5。
- 二叉堆 (MinHeap)
- 基石
- 基于哈希的集合
- Dictionary 或 Hashtable 用于 cameFrom 和 costSoFar
这些集合对于存储和高效检索路径信息和累积成本至关重要。它们为插入、查找和更新操作提供了平均$O(1)$的时间复杂度,考虑到A*中频繁的访问模式,这一点至关重要。CodeProject文章特别指出,通过将关闭列表从ArrayList替换为Hashtable,性能得到了提升 1。 - Equals 和 GetHashCode 的重要性
对于用作Dictionary或HashSet中键的自定义Location或Node结构体/类,正确重写Equals和GetHashCode方法至关重要。如果没有正确的实现,哈希冲突会使平均$O(1)$的性能降级为$O(N)$,并且Dictionary查找可能无法找到等效的节点 7。
- Dictionary 或 Hashtable 用于 cameFrom 和 costSoFar
表1:A*优先级队列实现比较分析
| 优先级队列类型 | Enqueue 复杂度 | Dequeue 复杂度 | Decrease-Key 复杂度 | A*实际性能 | 内存开销 | C#实现注意事项 |
|---|---|---|---|---|---|---|
| 无序列表 | O(1) | O(N) | O(N) | 差 | 低 | 简单但慢,不推荐 |
| 有序列表 | O(N) | O(1) | O(N) | 较差 | 低 | 插入慢,不推荐 |
| 二叉堆 (MinHeap) | O(logN) | O(logN) | O(logN) | 优秀 | 中等 | 常见,BlueRaja/High-Speed-Priority-Queue-for-C-Sharp,.NET内置PriorityQueue<> |
| 斐波那契堆 | O(1) | O(logN) | O(1) (摊还) | 良好 (高常数) | 高 | 理论最优,但实现复杂,实际常数高 |
| 红黑树 (SortedSet) | O(logN) | O(logN) | O(logN) | 良好 | 中等 | System.Collections.Generic.SortedSet |
上述表格基于5中对各种优先级队列实现的渐近复杂度分析,这些分析直接适用于A*算法。以表格形式呈现这些信息,可以清晰、简洁地比较每种实现的理论性能特征,并解释为何二叉堆(或
MinHeap)通常是A*最实用和高效的选择。它还突出了所涉及的权衡,例如,斐波那契堆理论上更优的decrease-key复杂度可能由于常数因子较高而无法转化为更好的实际性能。此表格可作为选择合适优先级队列的宝贵决策工具。
内存与对象管理
- 节点的值类型(struct)
- 减少垃圾回收开销
在C#中,class实例是分配在托管堆上的引用类型,受垃圾回收的影响。而struct是值类型,通常分配在栈上或内联在其他数据结构中(例如数组、其他结构体)。通过将节点表示定义为struct而不是class,可以显著减少频繁堆分配和随后的垃圾回收周期所带来的开销。这对于A*中频繁创建和处理大量节点的性能关键循环来说,是一项至关重要的优化 1。 - 改善缓存局部性
当struct连续存储在内存中(例如在数组中)时,它们受益于更好的CPU缓存利用率。访问已在缓存中的数据比从主内存中获取数据快得多,从而带来整体性能提升。
- 减少垃圾回收开销
- 优化节点结构
- 最小化大小
除了使用struct之外,最小化每个节点struct的实际内存占用也至关重要,特别是对于可能隐式或显式表示数百万个节点的大型网格。CodeProject文章通过将节点结构大小从32字节优化到仅13字节来证明了这一点。这是通过删除冗余数据(例如,通过数组索引而不是在节点内显式存储坐标)和使用更紧凑的数据类型(例如,对于父节点链接使用ushort而不是int,假设坐标在ushort范围内)实现的 1。
- 最小化大小
CodeProject文章 1 明确指出通过将节点改为
struct来“减少垃圾回收开销”,这突出了C#等托管语言中一个关键但经常被忽视的性能方面。在典型的A*实现中,可能会实例化和丢弃大量的Node对象。如果这些是class实例,则每次分配都会增加垃圾回收器的压力。当GC运行时,它可能会引入不可预测的暂停(即使很短暂),这对实时应用程序有害。通过使用struct,节点被分配在栈上(对于局部变量)或直接嵌入到包含结构中(如数组或其他结构体),从而避免了单独的堆分配,从而显著降低了GC周期的频率和持续时间。这是一种微妙但深刻的优化,直接影响寻路算法的响应能力和可预测性。这一分析扩展到C#性能调优的一般原则:在性能关键的代码路径中最小化堆分配。这可能涉及使用struct、为频繁创建的对象实现对象池,或利用更新的.NET功能(如Span<T>)进行直接内存操作,所有这些都旨在减少GC压力并提高缓存局部性。
网格与地图表示增强
- 用于$O(1)$访问的计算网格
- 消除列表搜索
对于基于网格的寻路,CodeProject文章中识别出的最重要优化是引入了“计算网格”。它不再维护一个需要查找操作的单独的“关闭列表”(例如,Hashtable),而是使用一个二维数组(PathFinderNode[,])或一个线性一维数组(PathFinderNode)来直接存储每个网格单元格的状态。这允许通过使用其 (X, Y) 坐标作为索引,以O(1)(常数时间)访问任何节点的状态或成本信息。这完全消除了对单独的“关闭列表”查找的需求,并简化了“开放列表”的作用,使其仅管理待处理的节点 1。
- 消除列表搜索
- 线性数组转换
- 简化坐标访问
为了进一步增强计算网格,作者将固定的二维数组(PathFinderNode[,])转换为线性(一维)数组(PathFinderNode)。这简化了坐标转换(例如,index = y * width + x),并且与C#中的原生二维数组索引相比,有时可以带来更高效的CPU内存访问模式 1。
- 简化坐标访问
- 2的幂次方网格尺寸
- 利用位运算
一个非常巧妙的优化是限制网格的宽度和高度为2的幂次方(例如,64x64、128x128、1024x1024)。这允许使用位运算(例如,(y << log2_width) + x)而不是计算成本更高的乘法和除法运算来进行坐标转换(例如,将二维(x, y)转换为一维index)。位运算在CPU级别通常快得多 1。
- 利用位运算
“计算网格”、“线性数组转换”和“2的幂次方网格尺寸”的结合 1 展示了对网格问题优化深刻的理解。通过将二维坐标映射到一维数组,算法本质上实现了一种高效的空间哈希形式。当与2的幂次方约束结合时,坐标查找和转换可以使用闪电般的位运算来执行,而不是较慢的算术运算。这将通常为
O(logN)(对于基于哈希的集合)或O(N)(对于基于列表的)查找转换为直接的$O(1)$内存访问。这是访问模式的根本性转变,带来了巨大的性能提升,特别是对于大型网格。这种方法虽然特定于基于网格的寻路,但说明了高性能计算中的一个更广泛的原则:理解底层内存布局、CPU架构以及利用底层操作(如位移)可以解锁超越通用数据结构所能提供的性能增益。对于具有固定、规则结构的问题,直接数组访问和位运算技巧通常优于更抽象或通用的数据结构。
算法改进
- “忽略旧节点”策略
- 避免昂贵的移除操作
在A*中,可能会找到一条通往已添加到“开放列表”(优先级队列)中的节点的更短路径。一种天真的方法是搜索并移除优先级队列中旧的、成本更高的条目,这可能是一个昂贵的$O(N)或O(\log N)$操作,具体取决于优先级队列的实现。“忽略旧节点”优化避免了这种情况。相反,旧的、成本更高的条目只是留在优先级队列中。当这个旧节点最终出队时,通过检查“计算网格”中的costSoFar或“关闭”状态,会检测到已经处理了通往它的更好路径,并且旧节点会被简单地忽略。这显著简化了优先级队列的操作,使其仅限于Enqueue和Dequeue,避免了对现有元素进行昂贵的移除或“减少键”操作 1。
- 避免昂贵的移除操作
- 优化网格清理
- 递增状态值
对于需要频繁调用寻路的应用(例如在游戏循环中),将整个网格的“开放”或“关闭”状态重置为默认值(例如零)可能是一个耗时的$O(N)$操作。CodeProject实现引入了一个巧妙的优化:它不是物理上清除计算网格,而是在每次新搜索时递增“开放”和“关闭”状态值(或使用唯一的搜索ID)。如果节点的当前状态值低于当前搜索的唯一标识符,则其在先前搜索中的状态值在当前搜索中被视为“未研究”。这避免了完全的内存重新初始化,大大减少了寻路调用之间的开销 1。
- 递增状态值
- 启发式函数调优
- 对速度和路径质量的影响
启发式函数(h(n))的选择和实现深刻影响A*搜索的速度和结果路径的质量(最优性)。更准确(但仍可接受且一致)的启发式函数可以更直接地引导搜索到目标,探索更少的节点,从而加快计算速度。然而,过于复杂的启发式函数本身可能成为计算瓶颈 1。 - “破局”机制
当A*遇到多个具有相同计算$f(n)$成本的路径时,可以应用“破局”启发式。这个额外因素有助于算法做出“最佳猜测”,以继续朝着有希望的方向搜索,通常会产生更平滑、更“自然”的路径,并防止算法不必要地探索同样“好”但最终不那么直接的替代方案 1。
- 对速度和路径质量的影响
- 成本精度
- 整数与浮点成本
CodeProject作者对成本计算精度进行了实验,发现使用int进行成本和总成本计算(有效地丢弃小数)会使算法在使用浮点数时“慢约10倍”,而对于复杂地图,路径质量没有显著改善。浮点运算在某些架构上可能比整数运算慢,并且微妙的精度差异可能导致算法更频繁地重新评估节点。这说明了牺牲一些精度可以带来显著性能提升的权衡 1。
- 整数与浮点成本
- 移动限制
- 对角线
启用或禁用对角线移动(8个方向与4个方向)会影响搜索空间和路径外观 1。 - 重对角线
如果允许对角线移动,增加其成本(“重对角线”)可以阻止其使用,从而导致路径更趋向于正交 1。 - 惩罚转向
每次算法改变方向时增加少量成本,会导致路径更平滑、更“自然”,因为会惩罚过多的转向。这可能会增加计算时间,但会改善路径美观度 1。
- 对角线
- 重新开放关闭节点
- 最优性与速度
标准A*实现可能不会重新开放已移至“关闭列表”的节点。然而,如果发现通往“关闭”节点的新的、成本更低的路径,允许算法“重新开放”并重新评估该节点可以导致真正最优且更平滑的路径。这会增加计算时间,因为算法可能会多次访问和处理节点。对于实时应用程序,可能更倾向于稍微次优但更快的路径 1。
- 最优性与速度
“忽略旧节点”和“优化网格清理”技术 1 是算法巧妙性的典范,它超越了仅仅选择高效数据结构。 “忽略旧节点”通过利用A*最终会找到节点最低成本路径的事实,避免了优先级队列中昂贵的移除操作。这推迟并有效地消除了昂贵的列表操作。“优化网格清理”是一种巧妙的技巧,可以避免在连续寻路调用之间进行$O(N)$的内存重置,这在动态或频繁查询的场景中可能是一个显著的开销。它不是将内存清零,而是使用唯一的搜索ID或递增状态值,从而利用现有的内存状态。这些优化展示了对算法迭代性质、内存访问模式和C#运行时的深刻理解,寻找在微观层面减少冗余工作和昂贵操作的方法。这些技术表明,真正的性能优化通常需要多层方法。虽然数据结构提供了基础效率,但显著的收益也可以来自高度专业的算法调整,这些调整利用了问题和执行环境的特定特征。这通常涉及权衡:为了实际的、特定领域的速度而牺牲一些理论上的纯粹性或通用性。
预计算与缓存
- 预计算成本
对于图结构和边成本不经常变化的静态或半静态地图,预计算某些值可以显著减少运行时计算。例如,roy-t/AStar库明确指出,“大多数计算(如边成本)在构建图时就已预计算”,这在实际路径搜索时节省了时间。这会将计算负载从运行时转移到初始化阶段 3。
V. GitHub上领先的C# A*实现
roy-t/AStar
- 项目概述
该项目位于github.com/roy-t/AStar,被描述为C#中“基于A*算法的快速2D寻路库”。它支持任何面向.NET Standard 2.0或更高版本的.NET变体,确保了广泛的兼容性。一个关键的设计理念是它不依赖外部依赖项,使其轻量且易于集成。该库采用MIT许可证,鼓励开放使用 3。 - 关键优化
- MinHeap用于优先级队列
该库明确指出其优先级队列使用了MinHeap数据结构。这是高性能A*的基础选择,为添加和提取元素提供了高效的$O(\log N)$操作 3。 - 预计算成本
一个显著的优化是“大多数计算(如边成本)在构建图时就已预计算”。这会将计算工作从关键的寻路循环转移到图初始化阶段,从而缩短搜索时间 3。 - 图优先表示
尽管它提供了方便的网格类(Grids.CreateGridWithLateralConnections、Grids.CreateGridWithDiagonalConnections),但该库内部使用图进行所有底层寻路。这种抽象允许灵活地建模各种移动模式(例如,网格上的车、象或后棋移动),同时利用优化的图遍历算法 3。
- MinHeap用于优先级队列
- 性能基准
该存储库声称具有令人印象深刻的性能,指出“即使对于包含10,000个节点和40,000条边的大型图,该算法也能在10毫秒内找到路径”。这一定量声明突显了其对速度的关注以及其优化的有效性 3。 - 可用性与特性
该库旨在通过网格类抽象图的细节,从而易于使用。它支持定义遍历速度,允许加权路径,而不仅仅是简单的最短距离。它还提供了模仿防止切角等行为的选项,这是旧寻路器中的常见功能 3。
BlueRaja/High-Speed-Priority-Queue-for-C-Sharp
- 作为基础组件的作用
BlueRaja/High-Speed-Priority-Queue-for-C-Sharp GitHub存储库包含一个针对寻路应用优化的C#优先级队列 4。高效的优先级队列对于A*算法的性能至关重要。A*的效率在很大程度上取决于其管理“开放列表”的能力,该列表需要快速地插入新节点、更新现有节点的优先级以及提取具有最低成本的节点。如果优先级队列操作效率低下,即使A*算法的核心逻辑再优化,整体性能也会受到严重限制。因此,一个高性能的优先级队列是构建任何快速A*实现的基础。 - 实现细节
该项目提供了一个高度优化的优先级队列实现,具有以下关键特性:- 速度
它被描述为比其他C#优先级队列更快,特别适用于寻路场景 4。 - 易用性
该库易于使用,简化了开发人员的集成过程 6。 - 无外部依赖
它不依赖于第三方库,这降低了项目的复杂性并简化了部署 6。 - 许可
该软件在MIT许可下,可免费用于个人和商业用途 6。 - LINQ支持
它实现了IEnumerable<T>接口,提供了对LINQ的支持,使得数据查询和操作更加便捷 6。 - 单元测试
该实现经过了全面的单元测试,确保了其可靠性和正确性 6。 - 稳定优先级队列
它具有稳定的优先级队列实现,这意味着具有相同优先级的项目将按照它们入队的顺序出队,这在某些应用中可能很重要 6。 - 性能增强
在.NET 4.5下编译时,它利用了新的强制内联支持,以实现更快的速度 6。 - 分发
该项目已发布到NuGet,便于集成到其他项目中 6。 - 兼容性
它应适用于.NET 2.0及更高版本 6。 - 实现
该项目包含两种优先级队列实现:一种用于最大速度(无线程安全和安全检查),另一种更易于使用且更安全 6。 - 语言
该项目完全用C#编写 6。
- 速度
- 对性能的影响
该优先级队列对A*算法的整体性能贡献巨大。通过提供高效的入队、出队和优先级更新操作,它显著减少了A*算法核心循环中的时间消耗。例如,如果一个A*算法需要处理数百万个节点,那么每次操作从$O(N)降到O(\log N)$所带来的性能提升是指数级的。这种基础组件的优化,使得上层A*算法能够充分发挥其启发式搜索的优势,从而在大型复杂环境中实现毫秒级的路径查找。
CodeProject的“Fast PathFinder” (CastorTiu的实现)
- 历史意义与影响
CodeProject文章“A* algorithm implementation in C#” 1 详细介绍了CastorTiu的A*算法实现及其优化。该实现因其在性能方面的开创性工作而具有重要的历史意义。作者最初因找不到满足其项目特定需求的C# A*版本而开发此实现,旨在提供一个高性能且可重用的资源。该项目附带一个前端应用程序,允许用户试验各种参数并分析算法行为,这对于理解和调优A*算法非常有价值 1。 - 量化性能增益
作者对标准A*算法的性能感到沮丧,尤其是在大型网格上。主要瓶颈在于搜索开放和关闭节点列表所花费的时间。为了解决这个问题,作者进行了一系列关键优化,最终实现了惊人的性能提升:与标准算法相比,速度提高了300到1500倍。例如,一个标准算法需要131秒才能解决的地图,使用优化版本只需100毫秒。然而,这种显著的速度提升并非没有代价,优化版本对于1024x1024的网格需要额外约13MB的内存 1。 - 架构经验
CastorTiu的实现引入了多项创新,这些创新对于现代高性能A*实现仍然具有重要意义:- 数据结构优化
将开放列表从标准的ArrayList或List替换为优先级队列,以提高节点检索时间。关闭列表则替换为Hashtable,以实现更快的查找 1。 - 使用结构体
将节点从类切换为结构体,以减少垃圾回收开销,提高内存效率 1。 - 计算网格
最显著的优化是使用第二个“计算网格”来存储节点,从而允许通过其 (X, Y) 坐标进行$O(1)$访问。这消除了对关闭列表的需求,并简化了开放列表的作用,使其仅限于推送和弹出成本最低的节点 1。 - 内存减少
节点结构经过优化,通过删除冗余坐标数据、启发式值以及使用ushort而不是int作为父节点链接,将其大小从32字节减少到13字节 1。 - 线性数组
计算网格从固定二维数组(PathFinderNode[,])更改为线性数组(PathFinderNode),以简化坐标访问并消除来回转换 1。 - 2的幂次方网格
添加了一个约束,即网格宽度和高度必须是2的幂次方,从而可以使用更快的位运算(移位和逻辑运算)代替数学运算进行坐标转换 1。 - 忽略旧节点
当发现通往已在开放列表中的节点的新的、成本更低的路径时,作者决定将旧节点留在列表中,而不是移除或替换它。旧节点将具有更高的成本,并在稍后处理时,由于已被标记为关闭而直接忽略。这比在列表中执行移除操作快得多 1。 - 优化网格清理
为了避免在寻路调用之间耗时的计算网格清理过程,作者实现了一个系统,其中“开放”和“关闭”状态值在每次新搜索时递增2。这意味着先前搜索的节点状态在当前搜索中被有效地视为“未研究”,而无需重置 1。 - 变量作用域
局部变量被提升为成员变量,以在堆上一次性创建,避免在栈上重复创建和销毁 1。 - 成本计算
作者选择使用int进行成本和总成本计算,舍弃小数。因为虽然浮点数提供了更多细节,但它们导致算法更频繁地重新评估节点,使其速度慢约10倍,而对于复杂地图,路径质量没有显著改善 1。
- 数据结构优化
其他相关的C# A*项目(简要提及)
除了上述重点项目外,GitHub上还有其他值得关注的C# A*寻路实现,它们针对特定用例或提供了不同的功能集:
- TheCyaniteProject/PathFinder3D
该项目专注于3D A*寻路,其特点是不需要烘焙导航网格,并且可以与动态创建的地形(如MapMagic或其他)一起使用。这对于需要实时适应变化环境的3D游戏或模拟非常有用 10。 - kbrizov/Pathfinding-Algorithms
这是一个更通用的存储库,其中包含各种寻路算法的实现,包括A*。虽然可能没有像roy-t/AStar那样专门针对A*进行极致优化,但它为学习和比较不同算法提供了有用的资源 11。 - hugoscurti/hierarchical-pathfinding
该项目实现了Unity中的近最优分层寻路(HPA*)算法,并使用《龙腾世纪:起源》的地图进行测试。HPA*是一种高级技术,通过在不同抽象级别上规划路径来提高大型地图的寻路性能,适用于需要在大规模环境中进行高效导航的场景 10。
表2:选定C# A*实现的功能与优化比较
| 项目名称 | 主要关注点 | 关键优化技术 | 性能特性 | 内存权衡 | 环境类型 | 许可证 |
|---|---|---|---|---|---|---|
| roy-t/AStar | 2D网格和图寻路 | MinHeap、预计算成本、图优先表示 | 10,000节点/40,000边在10ms内 | 低 | 静态 | MIT |
| BlueRaja/High-Speed-Priority-Queue-for-C-Sharp | 高性能优先级队列 | 二叉堆、强制内联、无外部依赖 | 极快,降低GC开销 | 低 | 通用 | MIT |
| CodeProject "Fast PathFinder" | 高速网格寻路 | 计算网格、线性数组、2的幂次方网格、忽略旧节点、结构体、整数成本 | 300-1500x加速,13MB额外内存 | 高 | 静态 | 自定义 |
| TheCyaniteProject/PathFinder3D | 3D动态地形寻路 | 无需烘焙导航网格 | 实时动态 | 未指定 | 动态 | 未指定 |
| kbrizov/Pathfinding-Algorithms | 通用寻路算法 | 未指定 | 学习/比较 | 未指定 | 未指定 | 未指定 |
| hugoscurti/hierarchical-pathfinding | 分层寻路 (HPA*) | HPA*算法 | 大型地图高效寻路 | 未指定 | 静态 | 未指定 |
此表格总结了上述C# A*项目的主要特点、优化策略和性能概览,为开发人员在选择适合其特定需求的实现时提供了快速参考。
VI. 动态与复杂环境下的A*变体
标准A*在动态环境中的局限性
标准A*算法主要设计用于静态或已知环境。它在搜索开始时假定所有障碍物、成本和图结构都是已知的且不会改变。当环境发生变化时(例如,出现新的障碍物、现有障碍物移动或成本发生变化),标准A*算法无法有效适应。它需要重新从头开始计算整个路径,这在动态或未知环境中效率极低,尤其是在需要频繁重新规划路径的场景中。在多智能体系统中,A*在低密度情况下表现良好,但在高拥堵水平下,特定起始位置可能会出现问题,导致算法卡住并失败,这凸显了其在动态环境中的局限性。
D*与D* Lite简介
为了解决标准A*在动态环境中的局限性,开发了D*算法及其变体,如D* Lite(动态A*)。D*算法是一种寻路算法,用于机器人和自主系统在未知或动态环境中导航。它旨在处理环境变化并相应地重新规划路径,使其成为环境未知或不断变化的应用程序的流行选择。
D*算法是A*算法的扩展,它结合了前向和后向搜索,以高效地重新规划路径。它通过维护一个待处理节点的优先级队列来工作,并迭代处理队列中的节点,在必要时更新成本并重新规划路径。当环境发生变化时,D*算法通过更新受影响节点的成本来重新规划路径。
D*及其变体已被广泛用于自主机器人,包括火星探测器“机遇号”和“勇气号”。Field D*是D*的一种基于插值的变体,它将节点定义在网格的角点上,并使用线性插值使路径点可以位于网格边的任何位置。这样,它可以在非均匀环境中生成直接、低成本和平滑的路径,解决了传统网格寻路算法将机器人运动限制在少数几个离散方向(例如,0、45、90度)的问题,从而产生非自然、次优的路径。
D* Lite是D*算法的简化版本,更高效且易于实现。它在自主车辆和机器人领域得到了应用。与A*不同,D* Lite在动态环境中表现出卓越的适应性,通过实时重新计算路线,在所有测试场景中都成功完成任务而没有失败,这表明它适用于需要对环境变化有高成功率的应用。
C# D* Lite实现
在GitHub上,可以找到D* Lite的C#实现,例如Bastiantheone/DStarLite [7。该项目提供了D* Lite算法的C#实现,可用于在机器人探索地图时将其导航到目标坐标。该实现假定地图可以表示为具有可导航和不可导航地形的网格。默认情况下,机器人可以向前、向后和侧向移动,但不能对角线移动。不过,通过简单的代码修改,可以允许对角线移动或为不同移动添加不同成本 [7。
该实现要求创建一个继承自DStarLiteEnvironment接口的类,该类负责环境与算法之间的交互,包含MoveTo和GetObjectsInVision两个方法。例如,TestProgram.cs中提供了如何使用该实现的示例 [7。
选择正确的算法
选择合适的寻路算法取决于应用程序的具体需求和环境特性:
- 静态与已知环境
如果环境是静态的且所有信息在搜索开始时都已知,那么A*算法通常是最佳选择。它能够找到最优路径,并且通过本报告中讨论的各种优化,可以实现极高的性能。 - 动态与未知环境
如果环境是动态的,或者信息是逐步发现的(例如,机器人探索未知地形),那么D*或D* Lite等动态寻路算法是更合适的选择。这些算法能够高效地重新规划路径,而无需每次环境变化都从头开始计算。D* Lite因其简化和高效的特性,在需要快速适应环境变化的机器人和自主系统应用中特别受欢迎。 - 路径平滑度
对于需要更自然、平滑路径的应用(例如,自动驾驶),Field D*等基于插值的D*变体可能更优,因为它们允许路径点位于网格边上的任意位置,从而生成更平滑的轨迹。
VII. 实际应用与未来展望
选择最优实现
选择最优的A*实现需要根据项目的具体需求进行权衡。没有一个“一刀切”的解决方案,因为不同的优化策略会带来不同的性能特性和资源消耗。
- 网格尺寸与复杂度
对于小型或中型网格,即使是标准A*实现(例如Red Blob Games的基线版本),在经过优先级队列优化后也可能足够。但对于大型网格(例如1024x1024或更大),CodeProject的“Fast PathFinder”所展示的计算网格、线性数组和2的幂次方网格尺寸等优化变得至关重要,因为它们提供了$O(1)$的节点访问速度,显著减少了查找时间 1。 - 环境动态性
如果环境是完全静态的,A*是理想选择。如果环境会发生变化,但变化不频繁,或者可以接受短暂的路径重新计算延迟,那么一个高度优化的A*实现仍然可行。然而,对于环境持续变化或信息逐步发现的场景(例如机器人导航),D*或D* Lite等动态寻路算法是更好的选择,因为它们能够高效地进行路径重新规划。 - 内存约束
一些高性能优化(例如CodeProject的“Fast PathFinder”中的计算网格)会增加内存消耗。在内存受限的环境(例如嵌入式系统或移动设备)中,可能需要权衡速度以减少内存占用。在这种情况下,选择内存效率更高的优先级队列(例如某些二叉堆实现)和紧凑的节点结构变得更为重要 1。 - 路径质量要求
如果路径必须是严格最优的(最短或最低成本),则需要确保启发式函数是可接受且一致的,并且可能需要启用“重新开放关闭节点”等功能,即使这会增加计算时间 1。如果可以接受轻微次优但计算速度更快的路径,则可以调整启发式或禁用某些功能以提高性能。 - 开发与维护成本
高度优化的算法通常更复杂,开发和维护成本更高。选择一个成熟且文档完善的开源库(如roy-t/AStar)可以显著降低开发负担,同时仍能获得高性能 3。
基准测试您的解决方案
在任何性能关键型应用程序中,对寻路解决方案进行严格的基准测试至关重要。仅凭理论分析或通用基准测试结果不足以保证在特定应用场景下的性能。
- 使用BenchmarkDotNet
C#生态系统提供了强大的基准测试工具,如BenchmarkDotNet。该工具允许开发人员精确测量代码的执行时间、内存分配和CPU使用情况。它能够揭示隐藏的内存成本,例如接口调用、Lambda表达式、值类型装箱以及其他看似无害的代码所导致的堆分配 2。通过
BenchmarkDotNet,开发人员可以:- 量化优化效果:精确测量特定优化(例如,切换优先级队列、使用结构体)对性能的影响。
- 识别新的瓶颈:当一个瓶颈被消除后,BenchmarkDotNet可以帮助识别下一个性能瓶颈。
- 防止性能回归:在持续集成/持续部署 (CI/CD) 流程中集成基准测试,可以防止未来的代码更改无意中引入性能下降。
调试与可视化
理解算法行为和识别性能问题需要有效的调试和可视化工具。
- 实时进度显示
CodeProject的“Fast PathFinder”提供了一个前端应用程序,可以实时显示算法的运行过程,包括节点如何被开放和关闭 1。这种可视化对于理解算法的探索模式和识别低效区域非常有价值。 - 路径可视化
Red Blob Games的实现包含DrawGrid静态方法,用于可视化cameFrom数组,显示墙壁和路径方向 7。这种功能对于验证路径的正确性和直观地理解算法的输出至关重要。 - 内存分析器
使用C#的内存分析器(例如Visual Studio内置的分析器或JetBrains dotMemory)可以帮助识别和解决内存泄漏、过度分配和垃圾回收压力问题。这与使用结构体和优化节点结构等内存管理策略相辅相成。
寻路算法的新兴趋势
寻路领域仍在不断发展,新的研究和技术不断涌现:
- 多智能体寻路 (MAPF)
在许多真实世界场景中,多个智能体需要同时找到路径并避免相互碰撞。MAPF算法旨在解决这一复杂问题,例如基于冲突搜索 (CBS) 的方法。 - 连续空间寻路
传统的A*通常在离散网格上操作。然而,对于机器人和自动驾驶车辆,在连续空间中进行寻路以生成更平滑、更自然的轨迹变得越来越重要。Field D*就是其中一种尝试解决此问题的算法。 - 深度学习与强化学习
人工智能领域的最新进展正在影响寻路。深度学习模型可以学习复杂的环境表示,而强化学习可以训练智能体在动态环境中找到最优策略,尽管这些方法通常需要大量数据和计算资源。 - 分层寻路
对于超大型地图,分层寻路算法(如HPA*)通过在不同抽象级别上规划路径来提高效率。首先在高层规划粗略路径,然后在局部细化路径,从而显著减少搜索空间 10。
VIII. 结论
在C#中实现高性能A*寻路算法是一个多方面且细致的工程挑战。它超越了对算法基本原理的理解,深入到数据结构选择、内存管理和算法微调的复杂性。
本报告的分析表明,A*性能优化的核心在于高效的优先级队列。像二叉堆这样的数据结构,其对数时间复杂度的操作,是实现快速节点插入和提取的基础,从而显著优于简单的基于列表的实现 3。
BlueRaja/High-Speed-Priority-Queue-for-C-Sharp等专门优化的优先级队列库,为任何A*实现提供了关键的性能支撑 6。
其次,精细的内存管理至关重要。将节点表示为struct而非class,可以显著减少垃圾回收开销并改善CPU缓存局部性 1。CodeProject的“Fast PathFinder”所展示的节点结构紧凑化,将节点大小从32字节缩减到13字节,进一步体现了内存优化的重要性 1。
第三,对于网格环境,创新的网格表示和访问模式带来了巨大的性能飞跃。引入“计算网格”以实现$O(1)$的节点访问,结合线性数组和2的幂次方网格尺寸以利用位运算进行坐标转换,这些技术共同将寻路速度提高了数百甚至上千倍 1。
最后,巧妙的算法改进,如“忽略旧节点”策略(避免昂贵的优先级队列移除操作)和“优化网格清理”(避免在连续搜索之间进行耗时的内存重置),展示了对算法行为和C#运行时环境的深刻理解如何转化为显著的性能提升 1。
最终建议:
- 优先级队列为王:始终使用高性能的优先级队列实现(如二叉堆),而不是C#中简单的List或SortedList。
- 拥抱值类型:在可能的情况下,将A*节点定义为struct,以减少堆分配和垃圾回收压力,从而提高实时应用的响应能力。
- 网格优化:对于基于网格的寻路,考虑实现一个直接的“计算网格”以实现$O(1)$访问,并探索线性数组和2的幂次方网格尺寸以利用位运算。
- 精细调整:根据项目需求仔细选择和调整启发式函数、成本精度、移动规则和“重新开放关闭节点”等参数,以在路径最优性、计算速度和内存消耗之间找到最佳平衡。
- 环境决定算法:在动态或未知环境中,标准A*的局限性显而易见。在这种情况下,应优先考虑D*或D* Lite等动态寻路算法,它们能够高效地进行路径重新规划。
- 严格基准测试:使用BenchmarkDotNet等工具对您的寻路解决方案进行系统性基准测试,以量化优化效果,识别隐藏的性能成本,并确保在目标环境中达到预期的性能水平。
通过采纳这些经过验证的优化策略并利用GitHub上可用的高性能C#实现,开发人员可以构建出能够满足最严苛性能需求的A*寻路系统,从而在游戏、机器人和各种AI应用中实现流畅、高效的导航。
引用的著作
- A* algorithm implementation in C# - CodeProject, 访问时间为 八月 14, 2025, https://www.codeproject.com/Articles/15307/A-algorithm-implementation-in-C-
- Is Your C# Code Fast? Benchmarking to Find Hidden Costs - YouTube, 访问时间为 八月 14, 2025, https://www.youtube.com/watch?v=yIRBO7xQ43o
- roy-t/AStar: A fast 2D path finding library based on the A ... - GitHub, 访问时间为 八月 14, 2025, https://github.com/roy-t/AStar
- priority-queue · GitHub Topics, 访问时间为 八月 14, 2025, https://github.com/topics/priority-queue
- EliahKagan/Dijkstra: Visualizing Dijkstra's algorithm with various priority queues - GitHub, 访问时间为 八月 14, 2025, https://github.com/EliahKagan/Dijkstra
- BlueRaja/High-Speed-Priority-Queue-for-C-Sharp: A C ... - GitHub, 访问时间为 八月 14, 2025, https://github.com/BlueRaja/High-Speed-Priority-Queue-for-C-Sharp
- Implementation of A* - Red Blob Games, 访问时间为 八月 14, 2025, https://www.redblobgames.com/pathfinding/a-star/implementation.html
- Mastering Pathfinding with A-Star: A Practical Guide and C# Implementation - Medium, 访问时间为 八月 14, 2025, https://medium.com/@hanxuyang0826/mastering-pathfinding-with-a-star-a-practical-guide-and-c-implementation-f76f1643d8c3
- A* Pathfinding Data Structure - Stack Overflow, 访问时间为 八月 14, 2025, https://stackoverflow.com/questions/27832405/a-pathfinding-data-structure
- astar-pathfinding · GitHub Topics · GitHub, 访问时间为 八月 14, 2025, https://github.com/topics/astar-pathfinding
- a-star-path-finding · GitHub Topics, 访问时间为 八月 14, 2025, https://github.com/topics/a-star-path-finding
- Pathfinding Algorithms in C# - CodeProject, 访问时间为 八月 14, 2025, https://www.codeproject.com/Articles/1221034/Pathfinding-Algorithms-in-Csharp