做路径规划也好做游戏寻路也好只要涉及从地图上的A点走到B点这件事A* 这个名字迟早会摆到你面前。我在模拟项目X里第一次独立实现C版A算法时以为这只是一个广度优先加上贪心的小改进结果真正落地时才发现数据结构选型、启发式函数设计、边界情况处理每一处都值得细抠。这篇文章就把我实际用C实现A时的完整思路、核心代码和踩过的问题整理出来希望能帮你少走几次弯路。A* 之所以被称作启发式路径搜索的黄金标准并不是因为它算法本身有多么惊为天人而是它把已知代价和估计代价组合成了一个统一框架在保证结果最优的前提下把搜索范围压缩到了非常可观的程度。无论你是在写2D网格地图的游戏AI、室内导航Demo还是某个地图服务里的路径规划模块A* 都是那个值得优先考虑、也最容易被理解的基础方案。这篇文章适合三种人刚接触寻路算法、想弄明白A原理的初学者已经写过简单A但不知道代码性能瓶颈在哪的C开发者以及需要把A*嵌入实际项目、正在做方案选型和参数调优的工程师。1. 为什么是A*路径搜索问题与算法选型1.1 路径搜索到底在搜什么先把问题定义清楚。所谓路径搜索就是在一个图结构里找到一条从起点到终点的路径并且让这条路径的某种代价最小。这个图可以是游戏里的格子地图也可以是路网拓扑甚至可以是一个抽象的状态空间。每个节点代表一个可能的位置或状态每条边代表一次移动边上的权重代表移动的代价。如果地图很小比如10乘10的网格直接枚举所有路径也能跑出来。可一旦地图变成1000乘1000甚至更大路径数量会以指数级别增长暴力搜索完全不可行。这就是搜索算法存在的意义在不遍历全部状态的前提下用尽可能少的探索次数找到最短路径。这里有一个容易被忽略的关键点路径搜索不只是找得到路更重要的是找得快。一个在10秒钟内算出最优路径但是在实际游戏里根本没法用的算法和另一个在10毫秒内算出95%近似路径的算法在工程上后者的价值往往更大。A*之所以能成为黄金标准恰恰是因为它在最优性和搜索效率之间找到了一个理论上可量化的平衡点。1.2 A* 与其他搜索算法的横向对比先看BFS广度优先搜索。BFS按层向外扩展从起点一层层扫描直到碰到终点。它保证在权值相同的图中找到最短路径但搜索范围是一个以起点为中心的圆圈效率很低。如果起点和终点相隔1000格BFS至少要探索大约300万格才能找到路径这个数字在很多实时场景里是没法接受的。再看Dijkstra算法。Dijkstra是BFS在带权图上的推广它优先扩展当前累计代价最小的节点。Dijkstra保证最优解但它同样有一个问题完全不考虑目标在哪边扩展范围仍然是一个以起点为中心的圆。终点在正上方100格Dijkstra还会往左往右各扩展一大片。然后是贪心最佳优先搜索。这个算法只考虑当前节点到终点的估计距离每次都挑看起来离终点最近的节点走。它的搜索效率极高扩展范围像一支箭直射终点但代价是完全不考虑已经走过的路。在有障碍物绕路的情况下贪心最佳优先可能会找到一条表面近实则很远的路径甚至绕进死胡同。A*做的事情就是把Dijkstra的累计代价和贪心最佳优先的估计代价结合起来。每个节点都有两个值g(n)从起点到当前节点的实际已花费代价h(n)从当前节点到终点的启发式估计代价f(n) g(n) h(n)通过当前节点的预估总代价A每次从开放集合中选取f值最小的节点进行扩展。这个简单的改变让A的搜索范围从圆形收缩成了指向终点的椭圆形——离终点近的方向被优先探索离终点远的方向被自动忽略。在大多数地图场景下A*扩展的节点数远少于Dijkstra同时依然保留最优性保证前提是启发式函数设计正确。1.3 为什么C是A*的最佳载体算法本身与语言无关但C和A*的搭配几乎成了行业默认组合背后有几个很实际的原因。第一个原因是性能。A*的核心操作是从优先队列里不断取最小元素并频繁插入新元素。在1000乘1000的地图上这个过程可能执行数十万次。C的std::priority_queue基于二叉堆实现插入和弹出的时间复杂度都是O(log n)配合内存分布紧凑的vector底层存储在实测中表现非常稳定。而解释型语言在这种高频循环下光解释执行的开销就可能让性能慢一个数量级。第二个原因是内存控制。A*需要记录每个节点的g值、h值、父指针、开放/关闭标记。在C里这些数据可以紧密地放进结构体或数组里Cache命中率高。相比之下对象化的语言里每个节点自带大量元数据内存碎片和治疗成本都会拖慢速度。第三个原因是C的模板和函数对象能力让启发式函数可以非常灵活地替换。你可以把曼哈顿距离、欧氏距离或自定义的代价函数作为模板参数传入同一个A*核心不需要为每种启发式重写一遍搜索逻辑。这个设计在实际项目中非常常见也是C在算法工程化中的天然优势。2. 核心机制拆解启发式函数、开放表与代价模型2.1 代价模型g(n)、h(n)、f(n) 三者怎么配合理解A*的关键不是记住公式而是理解这三个值在搜索过程中各自扮演的角色。g(n)是已经发生的事实代表从起点到当前节点n累计消耗的代价。这个值由搜索过程逐步累加不会被估计左右。如果每一步的移动代价都是1那么g(n)就是从起点走到n一共走了多少步。h(n)是一种基于信息的推测它不可能完全等于真实剩余代价否则你就像开了天眼一样直接沿最短路走过去就好。h(n)的作用是给搜索一个方向感告诉算法往这个方向走可能更快到达终点。f(n)则把事实和推测加起来作为当前节点优先级排序的依据。每次循环中算法取出f值最小的节点进行扩展然后把它的相邻节点加入优先队列。这里有一个容易困惑的点A并不是每一步都选择g值最小的节点而是选择f值最小的节点。这意味着它允许暂时走一条累计代价更高的路径——只要这条路径的启发式估计显示它可能更快到达终点。这个暂时绕路的机制正是A能够跳出局部最优的核心原因。2.2 启发式函数的可采纳性与一致性启发式函数h(n)不是随便选的它必须满足两个重要性质才能保证A*找到最优路径。第一个性质是可采纳性Admissible。一个可采纳的启发式函数永远不会高估从当前节点到终点的真实代价。也就是说h(n) ≤ h*(n)其中h*(n)是真实的最短剩余代价。如果h(n)高估了代价那么f(n)也会被高估算法可能会因为低估某个节点而错过真正的最优路径——终点看起来很远于是算法绕到了一条实际上更差的路。生活化的类比是你在地图上查路线启发式函数就是路况播报员。一个诚实可靠的播报员会告诉你至少有XX公里绝不会说肯定只有XX公里但最后实际跑了更多。如果播报员经常低估里程也就是高估剩余距离你很可能被误导选了一条糟糕的路。第二个性质是一致性Consistent也叫单调性。它要求h(n) ≤ c(n, n) h(n)其中n是n的某个邻居节点c(n, n)是从n移动到n的代价。一致性比可采纳性更强它保证了随着搜索进行f值不会递减。这个性质带来的实际好处是每个节点最多只需要被扩展一次。如果启发式函数只满足可采纳性但不满足一致性A*可能需要重新处理已经在关闭集合中的节点来保证最优性这会显著降低性能。在实际工程中绝大多数常用启发式函数曼哈顿距离、欧氏距离、八方向对角距离都同时满足可采纳性和一致性所以一般不需要专门验证但理解这个性质对排查为什么结果不是最优的问题非常重要。2.3 曼哈顿距离、欧氏距离、对角线距离怎么选不同地图类型对应不同的启发式函数选错的话要么求不出最优路径要么搜索效率大打折扣。**曼哈顿距离Manhattan Distance**适用于只允许四方向移动的网格地图。计算公式h(n) |dx| |dy|其中dx是当前节点与终点在x轴上的差值dy是y轴差值。为什么曼哈顿距离是可采纳的因为在四方向地图中每一步只能横向或纵向移动一格真实剩余代价不可能小于横向总步数加纵向总步数所以h(n)永远不会高估真实代价。**对角线距离Diagonal Distance**适用于允许八方向移动的网格地图比如很多战棋游戏和RTS游戏。计算公式需要考虑对角移动的代价h(n) max(|dx|, |dy|)当对角移动和直行代价相同 h(n) (|dx| |dy|) (√2 - 2) * min(|dx|, |dy|)当对角移动代价为√2第一个公式也叫切比雪夫距离实现最简单第二个公式更精确地考虑了斜线移动的实际代价。在大多数八方向网格游戏中使用第二个公式能让A*扩展更少的节点路径也更贴近真实最短路径。**欧氏距离Euclidean Distance**适用于允许任意角度移动的连续地图h(n) sqrt(dx² dy²)但这个函数在网格地图中使用时要格外小心。因为网格上从一点到另一点的真实路径不可能少于直线距离所以欧氏距离是可采纳的。然而在四方向网格中欧氏距离严重低估真实代价——比如终点在斜对角100格之外欧氏距离才141但真实走法至少要200步。低估意味着启发式函数不锐利A*的搜索范围会膨胀得很大几乎退化成Dijkstra。在网格地图上除非允许真正的自由移动否则优先选择曼哈顿或对角线距离。2.4 开放表与关闭表的作用实现A*时必须维护两个集合开放表Open Set和关闭表Closed Set。开放表存放已经被发现但尚未扩展的节点。每次循环从开放表中取出f值最小的节点。关闭表存放已经被扩展过的节点这些节点的最短路径已经确定在一致性条件下后续不需要再处理。这里有一个初学者常犯的错误把开放表和关闭表实现成两个容器的直接拷贝或链表遍历。在小地图上这种实现能跑但在大地图上性能是灾难性的。正确做法是开放表使用优先队列但为了性能优化通常不直接在优先队列里修改已存在节点的优先级而是采用惰性删除策略——新的更优路径被发现时直接把新节点push进队列旧的节点留在队列里但不处理通过一个标志位来判断是否已经过期。关闭表使用哈希表或二维数组标志。网格地图中可以用二维bool数组直接标记比哈希表快一个量级大型图结构中才考虑使用哈希集合。3. C工程实现数据结构选型与完整代码3.1 点与地图的建模在C里实现A*第一步是决定节点和地图的数据结构。我的推荐是简单直接不引入过度设计。#include vector #include queue #include unordered_map #include cmath #include algorithm #include cstdint // 网格地图0 表示可行走1 表示障碍物 using Grid std::vectorstd::vectorint; // 二维坐标点 struct Point { int x, y; bool operator(const Point other) const { return x other.x y other.y; } bool operator!(const Point other) const { return x ! other.x || y ! other.y; } };这里没有用pairint,int因为自定义结构体后续可以方便地扩展其他属性比如方向标记、语义标签。如果你用的是性能敏感的超大地图把x和y打包进一个uint32_t高16位存x低16位存y可以进一步减少内存拷贝对于10万节点的地图效果非常明显。地图用二维vector就够了。如果地图是动态加载或瓦片式的可以换成vectoruint8_t并用index y * width x访问内存更紧凑。3.2 优先队列的实现细节std::priority_queue默认是最大堆而A*需要最小堆所以必须自定义比较器。这里有一个非常隐蔽的坑priority_queue的Compare语义与std::sort相反——Compare返回true表示第一个参数优先级低于第二个参数。// 搜索节点包含坐标、g值、h值、f值 struct Node { Point pos; double g; double h; double f; Node(Point p, double gg, double hh) : pos(p), g(gg), h(hh), f(gg hh) {} }; struct NodeCompare { bool operator()(const Node* a, const Node* b) const { // 返回true时a排在b之后即f值大的在队尾 // 这是priority_queue小顶堆的正确写法 if (std::abs(a-f - b-f) 1e-6) { return a-g b-g; // f相等时g值大的优先更接近目标 } return a-f b-f; // f值小的优先 } };很多人在这一步把比较器方向写反导致A*优先扩展f值最大的节点搜索效率直接退化为最差情况。调试时可以打印每次弹出的f值确认它们是递增的。3.3 完整A*核心代码下面给出一个可以直接编译的完整实现。为了可读性我用值传递网格和坐标实际项目中可以根据地图大小换成引用传递。class AStar { public: AStar(const Grid grid) : grid_(grid) {} // 主入口返回路径点列表如果不可达则返回空vector std::vectorPoint findPath(Point start, Point end) { int rows grid_.size(); int cols grid_[0].size(); // 守卫检查 if (!isWalkable(start) || !isWalkable(end)) { return {}; } if (start end) { return {start}; } // 清零map记录每个节点的状态 // 0 未访问1 关闭2 在开放表中 std::vectorstd::vectorint state(rows, std::vectorint(cols, 0)); std::vectorstd::vectordouble gCost(rows, std::vectordouble(cols, INFINITY)); std::vectorstd::vectorPoint cameFrom(rows, std::vectorPoint(cols, Point{-1, -1})); // 优先队列存储指向节点的指针这里使用new实际项目中建议用索引 std::priority_queueNode*, std::vectorNode*, NodeCompare openQueue; Node* startNode new Node(start, 0.0, heuristic(start, end)); gCost[start.y][start.x] 0.0; state[start.y][start.x] 2; openQueue.push(startNode); // 八方向移动向量 const int dx[8] {1, -1, 0, 0, 1, 1, -1, -1}; const int dy[8] {0, 0, 1, -1, 1, -1, 1, -1}; while (!openQueue.empty()) { Node* current openQueue.top(); openQueue.pop(); Point currPos current-pos; // 惰性删除如果这个节点已经在关闭表中跳过 if (state[currPos.y][currPos.x] 1) { delete current; continue; } // 到达终点 if (currPos end) { std::vectorPoint path reconstructPath(cameFrom, start, end); // 清理所有剩余节点 while (!openQueue.empty()) { delete openQueue.top(); openQueue.pop(); } delete current; return path; } // 标记为关闭 state[currPos.y][currPos.x] 1; // 遍历八个邻居 for (int i 0; i 8; i) { Point nextPos{currPos.x dx[i], currPos.y dy[i]}; if (!isWalkable(nextPos)) continue; // 移动代价斜向为sqrt(2)直向为1 double moveCost (dx[i] ! 0 dy[i] ! 0) ? 1.41421356237 : 1.0; double tentativeG current-g moveCost; // 如果该邻居未访问或找到了更短路径 if (state[nextPos.y][nextPos.x] 0 || (state[nextPos.y][nextPos.x] 2 tentativeG gCost[nextPos.y][nextPos.x])) { gCost[nextPos.y][nextPos.x] tentativeG; cameFrom[nextPos.y][nextPos.x] currPos; if (state[nextPos.y][nextPos.x] 0) { Node* neighbor new Node(nextPos, tentativeG, heuristic(nextPos, end)); state[nextPos.y][nextPos.x] 2; openQueue.push(neighbor); } else { // 节点已在开放表中但发现了更优路径 // 惰性删除策略直接push一个新的旧的标记为过期 Node* neighbor new Node(nextPos, tentativeG, heuristic(nextPos, end)); openQueue.push(neighbor); } } } delete current; } // 清理优先队列中可能剩余的节点 while (!openQueue.empty()) { delete openQueue.top(); openQueue.pop(); } return {}; // 无可达路径 } private: const Grid grid_; bool isWalkable(Point p) const { if (p.y 0 || p.y static_castint(grid_.size())) return false; if (p.x 0 || p.x static_castint(grid_[0].size())) return false; return grid_[p.y][p.x] 0; } // 启发式函数对角线距离 double heuristic(Point a, Point b) const { int dx std::abs(a.x - b.x); int dy std::abs(a.y - b.y); // 8方向移动对角移动代价约1.414 // 这里使用带系数的对角距离公式 return std::max(dx, dy) (1.41421356237 - 1.0) * std::min(dx, dy); } std::vectorPoint reconstructPath( const std::vectorstd::vectorPoint cameFrom, Point start, Point end) const { std::vectorPoint path; Point current end; // 从终点回溯到起点 while (current ! start) { path.push_back(current); current cameFrom[current.y][current.x]; // 安全保护如果回溯超出地图或遇到无效点 if (current.x -1 current.y -1) return {}; } path.push_back(start); std::reverse(path.begin(), path.end()); return path; } };这段代码里有两个地方值得重点解释。第一是惰性删除策略。节点可能在多个地方以不同g值被push进队列我没有在发现更好路径时去更新队列里已有的老节点而是直接push一个新节点然后通过检查state标志来跳过已经关闭的节点。这个技巧能够大幅简化实现同时避免在优先队列里做修改元素优先级这种时间复杂度不确定的操作。代价是队列里可能存在一些废节点但在实际场景中废节点占比通常低性能收益远大于浪费。第二是比较器里的epsilon判断。f值相等时我选择g值更大的节点优先。这是因为g值大意味着当前更靠近终点优先扩展这些节点通常能更快找到目标。这算是一个微优化在一些路径长度相近的地图上能明显减少扩展节点数。3.4 路径重构的细节路径重构使用cameFrom数组从终点向前回溯。这个数组在搜索过程中记录了每个节点的来时路。有一个细节经常被忽视**cameFrom数组在网格地图上直接用Point存父节点会浪费8字节空间但换来了极简的代码和极高的可读性。**在极端性能要求下可以退化为只记录上一步方向一个uint8_t数字然后通过方向反向推出父节点坐标这样每个格子只占1字节。这个技巧在百万节点级别的寻路中很有价值但对于一般项目Point版本已经足够快。4. 参数调优与启发式权重从能跑到跑得快很多教程在写完代码后就结束了但真正在实际项目里A的性能和路径质量很大程度上取决于参数调优。这也是A作为黄金标准的另一层意思它给了你一个清晰的调优框架而不是一个不可碰的黑盒。4.1 启发式权重的意义与取舍A*的f(n) g(n) h(n)可以改写成f(n) g(n) w * h(n)其中w是启发式权重。当w 1时A*保证最优路径。当w 1时启发式对搜索的影响变大算法会更激进地朝终点方向扩展速度显著提升但代价是可能放弃真正的最优路径。这个控制在游戏开发中特别常见。玩家通常不会注意到3%的路径长度差异但会明显感受到NPC绕路时的呆滞感——实际上那往往是搜索范围太大导致的延迟。某游戏项目里的敌人寻路最初的实现是w 1的严格A*在复杂地形里每帧要跑好几毫秒。把w提升到1.3之后寻路耗时降到0.4毫秒以内路径长度增加也不到5%玩家反馈反而认为寻路更聪明了。如果你在做的是精确导航类应用比如室内机器人路径规划w必须严格等于1。但如果是游戏、模拟器或对实时性有要求的系统完全可以把w放到1.2到1.5的范围。4.2 移动代价的建模与调参移动代价是另一个容易被低估的调参维度。网格地图中相邻格子的移动代价可以是常量1也可以根据地形动态变化沼泽地代价3、道路代价0.5、爬坡代价5。A*对任意正数代价都能正常工作只需要保证启发式函数不超过真实代价的下界。这里有一个非常实用的技巧**当你为适应地形引入移动代价加成后启发式函数也要跟着调整。**比如在网格中引入一个所有格子基础移动代价都是2的修改那么曼哈顿距离h(n) 2 * (|dx| |dy|)而不是原来的1倍。否则启发式严重低估真实代价搜索范围膨胀性能骤降。我实际踩过的坑是在某个地图服务Demo中给草地加了1.5倍的移动代价但启发式函数没改。结果A*扩展了几乎和Dijkstra一样多的节点才找到路径性能从几毫秒掉到几百毫秒。后来把h(n)也乘以1.25才恢复正常。记住g值变了h值的下界也要同步变。4.3 稀疏地图与紧凑地图的微调在地图布局差异很大的场景还可以考虑两种优化策略。一种是Jump Point Search跳点搜索。在均匀代价的网格地图上JPS可以跳过大量对称路径将A*的性能提升一到两个数量级。但JPS只适用于代价均匀的网格一旦引入地形代价加成JPS就失效了。另一种是HPA*分层路径搜索。把大世界分割成多个区域先在抽象层区域间连线做一次A*然后在具体层区域内部细化路径。这种做法在大型开放世界地图中几乎是必备的但代码复杂度比单一A*高不少。如果项目刚起步我的建议是先从标准A开始把代码和数据访问模式优化到极限再考虑是否需要引入JPS或HPA。很多时候简单的A*加上正确的数据结构已经能跑赢被复杂化的优化算法。5. 常见问题与排查技巧实录5.1 路径不是最优的那些坑问题现象A*能跑出路径但路径明显绕远不是最短路线。排查步骤检查启发式函数是否满足可采纳性。最常见的错误是使用了没有乘以系数的对角线距离公式比如直接用max(|dx|,|dy|)代替带斜率的对角距离在某些特殊地图上会导致h(n)被高估。检查移动代价是否一致。如果八方向移动中直行代价是1斜行代价实际是1.4但启发式函数用的是直行代价2进行估计h(n)就会被低估导致搜索范围过大——这不是最优性被破坏而是性能变差的表现。区分这两种情况很重要。检查cameFrom的赋值时机。如果只在发现更低g值时才更新cameFrom必须确保新路径确实更短否则可能在回溯时连接到了非最优路径。我遇到过最隐蔽的一个问题在某地图上两个不同方向到达同一个格子的f值恰好相等比较器里g值大的优先这个微优化导致路径选择了看起来更绕的那条。后来验证才发现路径的总代价是一样的只是几何表现不同。如果这个更绕语义上没有实际代价损失在游戏里是可以接受的。5.2 死循环与不可达问题A*最常见的崩溃原因是不可达路径下的死循环或内存膨胀。起点周围被障碍物完全围住时开放表会被不断填充但永远无法扩展到终点最后耗尽内存或超时。解决这个问题的标准做法是在主循环开始前做一次基础的可达性预判断。对于网格地图可以用一个简单的泛洪填充Flood Fill从起点出发标记所有可达点如果终点不在可达集合中直接返回空路径。这个预判断的成本是O(N)相比A*在不可达地图上的O(N log N)运行成本要低得多。另一个可能的死循环原因是我之前提到的cameFrom回溯保护。如果cameFrom初始化时给了无效的Point{-1, -1}回溯时要加上检查否则当终点在逻辑上不可达时回溯会一直循环到内存越界。5.3 大数据量场景下的性能优化清单当网格规模超过1000乘1000时A*的每一步都值得优化。以下是我实测有效的优化清单按性价比从高到低排列把二维坐标索引化用uint32_t index y * width x代替Point结构体所有数组都用vectoruint32_t访问减少内存拷贝和Cache Miss。用数组替代哈希表g值、h值、状态标志都用二维数组或一维数组直接存储不要用unordered_map。数组访问的时间复杂度是O(1)且常数极小哈希表有明显的哈希计算开销。优先队列中存节点索引而非Node对象每次push对象new一个Node再delete它在几十万次循环下有不可忽略的开销。改用索引存储g和f值从全局数组中读取可以省掉大量动态内存分配。考虑使用双向A*从起点和终点同时扩展搜索当两个方向的开放表相遇时合并结果。在长距离路径规划中双向A*可以减少一半左右的扩展节点数。预处理地图数据如果地图是静态的可以预先计算地形代价并存储避免在每次寻路时重复读取和判断。这些优化每一项都能带来数倍的性能提升叠加起来效果非常显著。实测在某公司的路网项目里应用了这些优化后A*在一个10000节点路网上的单次平均搜索时间从8毫秒降到了0.3毫秒。5.4 路径平滑与视觉质量A*给出的路径是网格形式的一条折线在视觉上往往显得僵硬。NPC走起来会频繁转向机器人导航时也会因为直角转弯而降低通过性。这一点在游戏项目中尤其重要。我常用的平滑方法有两种。第一种是拐角简化Funnel算法在一系列路径点之间找到更直线的通视路径把折线变成尽量少的长线段。第二种是样条插值用Catmull-Rom或贝塞尔曲线将路径点拟合成平滑曲线让NPC走起来更自然。但要注意平滑后的路径不应该重新做碰撞检测。我曾在一个项目里对平滑后的曲线直接应用结果NPC在窄通道里因为曲线偏离网格而穿墙。正确做法是平滑后逐段做碰撞检测如果某段穿过障碍物就退回到原始的A*折线路径。最后再分享一个实操中的体会写A实现本身不难难的是理解它的边界条件和性能瓶颈。第一次在我负责的路径规划模块里跑通A时我以为事情已经结束了后面才发现调启发式权重、优化优先级队列、处理不可达地图这些才是真正的重头戏。经验之谈就是*在地图上跑一个效率低下的A不如在选择正确的数据结构和启发式函数上多花时间。**基础代码很容易从教程里复制但那些为了适配你的具体场景而做的小调整才是决定一个寻路系统是否好用的关键。如果是在游戏、仿真或者需要快速迭代的项目里我建议你从标准A开始验证逻辑正确性后再逐步加入优化。盲目追求跳跃点搜索或分层寻路很容易在功能跑通之前就把代码复杂度搞到失控。而对地图规模较小的项目标准C A已经足够快优化思路可以作为知识储备不必强行应用。这个算法之所以能拥有黄金标准的名号靠的正是这种简单可靠、可预测、可调优的特性。
阅读完成 · 觉得有帮助?