2026-09-11

从最短路到路径构建:图是怎么被定义出来的

三个词不是并列关系

最短路径、寻路、路径构建,看起来是同一类话题的三个说法。实际上它们是同一个问题在三层抽象上的展开,而这三层的难度和成熟度差异极大:

层次 要回答的问题 图的状态 核心难点 成熟度
最短路 给定图,求最优解 已知、静态 基本已解决(1959) 教科书
寻路 图太大 / 未知 / 会变 隐式、动态 启发式从哪来 工程化
路径构建 图本身还没定义 要你自己构造 什么是节点?什么是边? 开放

这个分层有一个很重要的推论:绝大多数路径规划项目的瓶颈从来不是最短路算法,而是图怎么定义。

选 Dijkstra 还是 A*,通常只影响 10% 的运行时间;而节点、边、边权的定义方式,直接决定整个系统的上限。下面逐层展开。


一、最短路:算法选择由约束唯一决定

先把算法家族摆清楚。真正决定用哪个算法的不是"哪个更快",而是三个约束:边权的符号、图的结构、查询的频率。

约束 算法 复杂度
无权图 BFS O(V+E)
非负权 Dijkstra + 二叉堆 O((V+E)logV)
可含负权 Bellman-Ford O(VE)
DAG 拓扑序 DP O(V+E)
全源、稠密 Floyd-Warshall O(V³)
全源、稀疏 Johnson O(V²logV + VE)
次短路 / k 短路 Yen、Eppstein —

反直觉点一:堆优化不是永远更快

通常的叙述是"Dijkstra 用堆优化到 O((V+E)logV)",好像堆优化是标配。但在稠密图(E ≈ V²)上,朴素的 O(V²) 线性扫描实现反而更快——堆的 log 因子白白付出,还带来额外的常数开销和缓存不友好。

结论:图密度决定实现方式,而不是"堆优化就是好的"。

反直觉点二:DAG 上不需要 Dijkstra

如果图是有向无环的,拓扑序一遍扫描就能算出所有单源最短路,而且是线性的,并且天然支持负权。

所以在动手之前,先问一句"我的图有环吗",往往比问"该用哪个算法"价值大得多。很多真实问题的图结构其实自带很强的约束(依赖关系、流水线阶段、时间展开),这些约束直接消掉了大部分算法选择。

工业界真正的答案:预处理,而不是更快的搜索

一个常被忽略的事实:高德、Google Maps 这类导航系统每天要处理几十亿次路径查询。每次跑一遍 Dijkstra 是完全不可想象的。

它们用的是「预处理 + 极快查询」的路线:

  • Contraction Hierarchies(CH):按"重要性"给节点排序,依次把不重要的节点"收缩"掉,并为被绕过的路径补上捷径边。查询时只在剩下的高层图上搜索,再展开捷径还原成真实路径。
  • Hub Labeling:预计算每个节点到一组 hub 的距离标签,查询时只做标签交集。
  • CRP(Customizable Route Planning):把预处理拆成"拓扑预处理"和"权重定制"两阶段,从而支持路况实时更新。

这些方法在大陆级路网上能把查询从秒级压到毫秒乃至微秒级,三到五个数量级的加速。

背后的结构性先验非常朴素:几乎所有的长距离出行都是「先上主干道或高速,到接近目的地时再下来」。把这个先验预计算进索引,搜索空间就从百万节点掉到几百节点。

这里的关键洞察是:最短路的真正瓶颈是「重复查询」,而不是「单次查询」。 只查一次,Dijkstra 就够;要查十亿次,预处理才划得来。这是一个典型的"用空间和预处理换查询时间"的工程权衡,而不是算法理论问题。


二、寻路:当图不可知或巨大

A* 的骨架是 f(n) = g(n) + h(n):已走代价加上对剩余距离的估计。真正有讨论价值的是 h 从哪来:

  • 几何启发式:欧氏距离、曼哈顿距离。简单,但在有障碍物时很松。
  • 地标法(ALT):预先挑若干 landmark 节点,利用三角不等式 |d(A,L) − d(L,B)| ≤ d(A,B) 构造下界。这是"用预处理换启发式强度"的又一例。
  • 学出来的启发式:Neural A*、用 GNN 直接从图结构预测 h。近年的研究热点。

反直觉点三:h 不是越准越好

这是我觉得最值得反复咀嚼的一点。

  • h 高估真实距离 → 不再 admissible → 失去最优性保证。搜索会变快,但可能返回次优解。
  • h 过强 → 退化成纯贪心。搜索变得近视,"看起来近"的节点被优先展开,全局结构被忽略。

你要的其实是:刚好乐观。宁可低估,不可高估。这个"乐观性"约束是 A* 正确性的全部来源。

与之配套的另一个概念是 consistency(一致性):满足 h(n) ≤ c(n,n') + h(n')。一致性比可采纳性更强,它保证节点不需要被重新打开。可采纳性保证最优,一致性保证效率——两者容易混淆,但作用完全不同。

网格地图上的两个必要技巧

  • JPS(Jump Point Search):网格上存在大量对称路径,JPS 利用这一点,一次跳跃掠过整段直线,把搜索规模砍掉一个数量级。
  • **Theta***:允许任意角度的路径段,避免路径被网格"咬"成锯齿。这类方法统称 any-angle path planning。

连续空间:干脆不建图

机器人领域的大量问题(机械臂、无人机、自动驾驶的局部规划)根本没有离散图。做法是采样:

  • **RRT / RRT***:从起点长一棵随机树,向采样点扩展,RRT* 额外做重连优化。
  • PRM(概率路线图):预先在自由空间撒点连边,建成一张稀疏图,之后在其上反复查询。

关键共同点:从不构建全局图,只在需要的地方生成局部结构。

环境会变:增量修复而非重算

D* Lite 是这一类问题的代表:当环境发生变化(新障碍物出现),它不重新规划,而是增量修复受影响的局部子图。火星车、仓储机器人用的就是这一族算法。


三、图构建:真正的难点在这里

前面两层都有成熟解法。到了这一层,问题变成"图从哪来",而这几乎总是项目成败的分水岭。

3.1 定义节点和边,本身就是建模决策

节点选什么?

  • 路口图(node = 交叉路口):最直观,但转向代价无处安放。左转和直行在图上都是"两条边的连接",可实际代价能差十倍。要塞进转向代价,就只能给每条边挂上入口边的信息,或者给每个转向建一个节点——图迅速膨胀。
  • 路段图(node = 路段):转向代价变成节点代价,干净利落,代价是节点数上升。
  • 线图(line graph):把原图的边提升为节点,是处理转向代价的标准做法。

这几种选择没有绝对优劣,只有"你的代价模型是什么"。

边权是什么?

距离?时间?拥堵概率?能耗?景观?一旦涉及多个目标,**"最短路"这个概念本身就不存在了**——剩下的只有 Pareto 前沿:在所有目标上都不被支配的解集合。

更麻烦的是时变图 w(e, t),见下一节。

3.2 图从数据里长出来

数据源 建图方式 典型脏数据
OpenStreetMap 拓扑路网提取 断头路、单行道方向、禁转关系、道路等级误标
激光 / 视觉 occupancy grid → 图 动态障碍、传感器噪声、标定误差
轨迹数据 从实际行驶路径反推边权 采样稀疏、异常轨迹、隐私脱敏
路况数据 给已有边赋时变权重 覆盖不均、延迟、口径不一

最后一行值得特别强调:从人的实际选择反推边权,会得到一个和"理论最短路"很不一样的图。这是第四节最后一条的伏笔。

3.3 隐式图:不构建,按需展开

这是我最想推荐的一个技巧。

有些状态空间根本不可能存下来:

  • 八数码:约 1.8×10⁵ 个可达状态,勉强能存,但已经不小
  • 魔方:4.3×10¹⁹ 个状态——无论用什么硬件都存不下。人类最终的解法是双向 BFS 配合模式数据库,用几十年时间算出了"上帝之数 = 20"(任意状态 20 步内可解)

但注意:这些问题的图在数学上真实存在,在程序里从不落地。你只需要提供一个生成函数:

def neighbors(state):
    """返回从 state 一步可达的所有后继状态"""
    ...

搜索算法只和这个函数打交道。整张图是"按需展开"的。

这个技巧在处理 agent 问题时是刚需,因为 agent 的状态空间同样是组合爆炸且无法枚举的。

3.4 图是"构造"出来的,不是"发现"的

"构建"二字真正的重量在这里。

层次图:先建骨架层(高速、主干),再挂细节层。CH 本质上就是把层次结构预计算出来。这类"分而治之 + 层次抽象"的思想在几乎所有大规模图问题上都会重现。

图嵌入:node2vec、DeepWalk 把节点映射到向量空间,让"相似"变成"距离"。这一步把建图问题转化成了表示学习问题——边不再需要显式定义,而是在嵌入空间里隐式存在。

超图(hypergraph):普通图的边只能连两个点。但"三位作者合写一篇论文"是一个三元关系,用普通图表示就必须拆成三条两两的边,信息就失真了。超图允许一条边连任意多个节点。

AND-OR 图:化学逆合成路径的典型结构。一步反应需要多个前体同时满足,是乘法关系而不是加法关系。这意味着最优子结构不成立——"到达某个中间产物的最优代价"无法独立定义,标准最短路算法直接失效。

多层图 / 时序图:边带类型(地铁、公交、步行可换乘)或带时间戳。城市多模态出行规划就是典型的多层图问题。


四、跳出"路"这个框架

"路径构建"是一个比道路导航宽得多的抽象。以下五个领域共享几乎完全相同的底层结构。

4.1 Agent 的工具调用 = 图上的一条路

把 agent 的决策过程形式化:

  • 节点 = (当前状态, 已调用的工具集合)
  • 边 = 一次工具调用
  • 最短路 = 以最少步数 / 最低代价完成任务

这与 A* 严格同构:f = 已花代价 + h(离完成的距离估计)。

而所有难点都落在图构建上:

  • 状态空间组合爆炸 → 是隐式图,必须提供生成函数而不是显式图
  • 边权是动态的:工具成功率、token 成本、延迟、是否触发人工审批
  • 节点不可逆:有副作用的操作不能"回退",图是有向的但带"毒"——走错一步的代价不是重新搜索,而是造成了真实世界的改变

这也解释了为什么工具编排(skill chain、工作流组合)本质上是在技能图上找路;而对这类系统做故障注入(mutation testing),本质上是给这张图注入边故障——两者研究的是同一个对象的两面。

4.2 LLM 推理 = 隐式图上的启发式搜索

这个类比我认为是本节最有价值的一条:

LLM 范式 图搜索中的对应物
Chain-of-Thought 贪心 / 随机游走
Tree-of-Thought beam search
ReAct 动作图上的交错搜索
o1 / R1 的长思维链 学出来的启发式函数 h

如果接受这个映射,那么一个有意思的结论自然浮现:

"幻觉"在搜索的语言里,就是 h 不可采纳——模型高估了"当前推理到正确答案"的距离。

于是"模型过度自信"和"启发式过强"变成了同一个失效模式:h 骗过了搜索,把它导向一条看起来短、实际不存在或通向错误终点的路。这个统一视角在讨论推理可靠性时相当有用,因为它把"模型能力问题"翻译成了"搜索正确性问题",而后者有几十年的理论工具可用。

4.3 肉鸽的通路:未知图 + 预算约束

Roguelike / 集成战略这类玩法,把路径问题的难度直接拉满:

  • 节点 = 房间 / 事件,边 = 可选路线,但图和代价都藏在战争迷雾里
  • 这是未知图上的在线搜索,天然包含 explore / exploit 权衡
  • 资源上限(希望、源石锭)构成预算约束 → 属于 Resource Constrained Shortest Path,NP-hard
  • 更根本的是:因为路线本身不确定,目标函数不是"路径最短",而是"期望收益最大" → 问题从最短路退化成 MDP 求策略

这是一个很好的例子,说明一旦引入不确定性,最短路框架就不够用了,必须升级到决策过程。

4.4 GPU 计算图

在编译器和 kernel 优化里:

  • 算子融合 = 在数据流图上找"合并收益最大"的路径
  • 融合的约束(显存占用、寄存器压力、shape 对齐)就是图上不可行的边
  • 代价函数高度非线性,可加性不成立(融合两个算子的收益不是各自收益之和)

所以这不是最短路问题,而是带约束的图划分 / 图重写问题。把它当成最短路来做会直接跑偏。

4.5 制度与审批流

一个组织里,一份文件过哪些环节,本质上是一条路径,边权 = 时效 / 权限 / 风险。

但这里有个关键差异:当规则本身可以在运行时改变时,图是动态的。制度即插件的系统里,节点和边都会在运行过程中增删——静态最短路算法需要升级为动态图上的增量维护。

而且这里想要的根本不是"最短路":真正的目标是"最合法、最不容易被驳回、最经得起追溯的路径"——天然多目标。


五、几个反复出现的"怪物"

以下六个坑我倾向于反复提醒自己:

1. 时变图破坏 FIFO 假设

如果边权随时间变化,且不满足 FIFO(早出发一定不晚到),Dijkstra 的正确性直接崩塌。而现实中这恰恰很常见:高峰期出发反而更慢、某条路的拥堵随时间演化、班次表本身就是时变的。

理论上很干净,实践上非常脏。这是几乎所有真实导航系统都要专门处理的坑。

2. AND-OR 结构:最优子结构不成立

一旦某个"步骤"需要多个前提同时满足,代价结构就从加法变成乘法或更复杂的组合,"到达某个中间状态的最优代价"这个中间量根本无法独立定义。动态规划的地基被抽掉了。

3. 资源受限最短路是 NP-hard

既要求路径代价最优,又要求资源(时间、油量、预算)不超限,属于 Resource Constrained Shortest Path,NP-hard。在实际工程中,近似算法和解的"够好性"比精确最优重要得多。

4. 脏数据才是主要矛盾

路径规划项目真实的成本结构大致是:90% 的时间在清理和校验数据,10% 的时间在跑算法。

拓扑错误、坐标漂移、属性缺失、口径不一致——这些才是让项目失败的东西。算法选型错误几乎从来不是。

5. 用户要的不是最短

人的实际选路系统性偏离最短路假设。人们偏好熟悉的路、少转向的路、景观好的路、不容易走错的路。

于是导航系统有时会故意推荐次优路径,因为用户"觉得"它更快,或者因为它更容易被理解和跟随。

一条可解释的次优路,往往胜过一次不可解释的最优路。

6. 图的定义比图的算法重要

这是贯穿全文的主线。当你发现某个路径规划效果不好时,先别急着换算法——回去看看节点是怎么定义的、边权代表什么、哪些真实约束没有被编码进去。


六、如果动手,建议的顺序

按"手感 → 震撼 → 理论 → 迁移"四步走:

第一步(约半天):建立图长什么样的手感

用 OSMnx 拉一个真实城市的路网,转成 NetworkX 图,跑一遍 Dijkstra 和 A*。重点不是学会 API,而是观察真实路网的度数分布、边权分布、连通性——真实图和数据结构的课本图相差很远。

第二步(约 2~3 天):亲手实现一次 CH,感受结构化先验的价值

自己写一遍节点收缩和捷径添加,然后对比查询耗时。当你看到几万倍加速时,会对"预处理换查询"这件事产生完全不同的直觉——这是全文最值钱的一课。

第三步(约 1 天):八数码,理解隐式图与启发式强度

用同一份 neighbors() 生成函数,分别跑 BFS、A*(弱 h)、A*(强 h)、IDA*,对比展开节点数和结果质量。亲手体会一次"h 太强会破坏最优性",比读十遍定义管用。

第四步:迁移到 agent 场景

把一次 agent 执行的工具调用日志构造成图(节点 = 状态,边 = 调用),跑最短路问"理论上最少几步能完成",再和实际路径对比。实际的冗余步数就是优化空间,而偏离最优的位置就是编排逻辑的薄弱点。


结语

最短路径、寻路、路径构建这三个词,本质上描述的是同一件事在不同抽象层上的样子。往下走到最短路,问题已经在 1959 年被解决,剩下的只是工程优化;往上走到路径构建,问题变成"什么才是一个节点"——而这从来不是算法问题,是建模问题。

同一套结构在导航、机器人、LLM 推理、agent 编排、GPU 编译、组织流程中反复出现,而且每次都是图的定义方式决定上限,算法选择只影响常数。

所以下次面对一个"找路"问题时,值得先问的不是"该用什么算法",而是:

我的节点是什么?我的边是什么?我漏掉了哪些真实约束?