算法的故事 / 第一季第二季目录0 / 30 章已读

第二季 · 第 44 章

给机器一双寻找方向的眼睛

《算法的故事》第二季 · 第三篇:让机器理解空间——1968年,A* 算法与第一次有方向的机器搜索1968年,加利福尼亚州门洛帕克。斯坦福研究所(Stanford Research Institute)的研究人员正在尝试一件极具挑战性的事情。他们希望造出一台真正能够理解周围环境,并自行规划行动的机器人。它有轮子。有摄像机。有测量距离的装置。能够接收指令。但它还远远不具备今天人们想象中的人工智能能力。一个对人类极其简单的任务,就可能让它陷入长时间的计算。例如:从房间的一端移动到另一端。如果中间存在障碍物,应该绕左边,还是绕右边?如果左边的道路看上去更接近目标,却被一堵墙挡住,是否需要回头?如果右边的道路起初远离目标,却最终能够通向终点,机器应该怎样理解这种绕行?计算机可以尝试所有可能路线。它也可以使用已有的最短路径算法,一层一层地探索地图。但它会花费大量计算,研究那些明显背离目的地的路线。于是,一个新的想法逐渐形成。机器在搜索时,能不能既记住已经付出的代价,又估计距离目标还有多远?三位人工智能研究者把这两种信息组合起来。他们是:彼得·哈特(Peter E. Hart)。尼尔斯·尼尔森(Nils J. Nilsson)。伯特伦·拉斐尔(Bertram Raphael)。1968年,他们发表了一篇经典论文:A Formal Basis for the Heuristic Determination of Minimum Cost Paths《启发式确定最小成本路径的形式化基础》。论文给出了一种搜索算法,并用数学方法分析它能够在什么条件下找到最优路径。它就是:

A*

A-star · A星搜索算法f(n)=g(n)+h(n)f(n)=g(n)+h(n)
一个看似简单的公式,改变了计算机寻找道路的方式。其中:
  • g(n)g(n):从起点走到当前节点,已经找到的最小路径成本。
  • h(n)h(n):从当前节点到目标,预计还需要付出的成本。
  • f(n)f(n):经过当前节点抵达目标的估计总成本。
于是,计算机不再只问:“我已经走了多远?”它还会问:“如果继续选择这条路,我最终可能需要付出多少代价?”这项思想后来进入电子游戏、路径规划、机器人、地图导航,以及许多人工智能搜索系统。但它绝不是简单地让机器人“朝着目标方向走”。因为真正优秀的A,不仅能够使用方向信息。*它还知道:什么时候,这份方向信息值得相信;什么时候,过分相信它反而会造成错误。

第一幕:一台会摇晃的机器人,想要自己找到路

计算机历史博物馆保存的 Shakey 机器人实验照片。它使用机载传感器感知环境,而复杂计算主要由远程大型计算机完成。[CHM Revolution]让我们先认识这个故事背后的机器人。它有一个十分朴素的名字:Shakey。中文可以叫它“摇摇”。这个名字的来源,并不是什么复杂的人工智能理论。据项目负责人 Charles Rosen 后来的回忆,团队花过一些时间讨论名称,最后有人注意到机器人移动时会摇晃,于是决定叫它 Shakey。这项轶事记录在计算机历史博物馆的展览资料中。[CHM Revolution]

一台没有今天机器人那样灵活的机器

Shakey 的开发工作主要发生在1966年至1972年。它是由斯坦福研究所团队研制的早期智能移动机器人。项目负责人是 Charles Rosen。美国国防部高级研究计划局的前身 ARPA 为项目提供了重要支持。研发团队希望将机器视觉、问题求解、规划与运动控制结合起来。[DARPA][CHM Revolution]Shakey 早期研究照片,研究者在机器人旁检查设备。图示设备与现代拥有大规模机载算力的机器人截然不同。这台机器有一个由轮子驱动的底座。装有电视摄像机、距离测量设备与触碰传感器。它能够通过无线电与远程计算机通信。但它没有今天智能手机那样强大的机载处理器。很多复杂的感知、推理与搜索工作,都由远处的大型计算机完成。Shakey 的实验环境也经过专门设计。研究人员使用房间、门口、坡道和可以移动的方块,构成一个能够控制复杂度的小世界。他们希望机器人完成一些类似这样的任务:移动到另一个房间。绕开障碍物。找到某个物体。把方块推到指定位置。这些任务对于人类来说十分普通。可对于1960年代的机器人,它们已经涉及许多尚未解决的人工智能难题。

找到方向,并不等于知道怎么走

假设机器人站在房间左侧。目的地在右侧。最直接的策略是什么?朝右走。但如果中间存在一堵墙呢?机器人就必须绕开它。假设墙的上方和下方都能绕过去。上方的路线可能更短。下方的路线可能更长。而机器人又不一定能够直接看见全部通路。这时,仅仅知道“目标在右边”是不够的。机器必须把环境表示成一个可以搜索的结构。最常见的抽象方式,是图(Graph)。我们可以把不同位置作为节点。把允许执行的移动作为边。边上的权重表示移动成本。
图44-1:把房间变成一张可搜索的图
SGS = 起点;G = 目标;深色方格 = 障碍物
查看 SVG 配图原件
示意图原件 44-004-425e2caf9d82.svg
44-004-425e2caf9d82.svg · 点击查看大图
教学示意:机器人必须绕过中间障碍。真实 Shakey 的导航、感知与规划系统远比这张简单网格复杂。
现在,我们就拥有了一项清晰的计算问题。给定:G=(V,E)G=(V,E)其中 VV 是所有可到达状态,EE 是允许的移动。每条边有非负成本:c(u,v)≥0c(u,v)\ge0寻找从起点 ss 到终点 tt 的最低成本路径。这项问题在A*出现之前,早已受到数学家研究。那么,1968年的三位研究者究竟增加了什么?

Shakey 与 A* 的真实历史联系

计算机历史博物馆明确记录:Shakey 项目中发展出的搜索技术,包括 A*。机器人通过规划路径上的中间位置(waypoints)寻找绕过障碍物的路线。项目还产生了后来广为人知的 STRIPS 自动规划系统。[CHM Revolution]但必须区分:Shakey 不是一个仅仅运行A*的机器人。A*也不是 Shakey 的完整智能系统。机器视觉负责感知。规划系统负责推理行动目标。搜索算法可以帮助求解其中的路径或状态搜索问题。实际运动控制又是另一项工作。因此,更准确的历史解释是:Shakey 所处的机器人研究计划,为A等人工智能搜索方法提供了重要的研究背景与应用需求;A后来成为其中具有长久影响力的一项算法成果。还有一个值得记住的时间细节。A* 论文正式发表于1968年7月。但论文首页标注的收稿日期是:1967年11月24日。 [ai.stanford.edu]所以,虽然我们常说A*诞生于1968年,但它的研究成果在1967年就已经形成并提交。而这个故事更早的数学基础,则来自一位我们已经很熟悉的研究者。

第二幕:1956年的荷兰铁路,给A*留下了第一块基石

埃兹赫尔·迪杰斯特拉(Edsger W. Dijkstra)。他在1950年代研究最短路径问题,1959年发表了影响深远的图算法论文。让我们先把时间倒回1956年。地点是荷兰阿姆斯特丹。在那里,一名年轻的计算机科学家正在为一台计算机的公开演示寻找合适的问题。这名研究者是:埃兹赫尔·迪杰斯特拉。他后来在1982年的亲自回忆中谈到,当时数学中心正在准备展示新计算机的能力。演示需要一个普通观众能够理解的任务。他选择了铁路路线。工作人员可以提供一张简化的荷兰铁路网络地图。观众提出两个城市的名称。计算机则输出连接它们的最短路线。例如,从荷兰北部某座城市前往南部某座城市。迪杰斯特拉回忆,这项演示取得了成功。而他正是在1956年发现了后来以自己名字命名的最短路径方法。论文则直到1959年才发表。[加菲尔德图书馆][Springer Nature Link]

迪杰斯特拉的核心思想

假设我们从起点S出发。每条道路都有非负长度。我们希望找到各个地点距离S的最短距离。算法先从S开始。然后扩展当前已知距离最小的未确定节点。每当发现一条更便宜的路径,就更新对应节点的记录。这样,一圈又一圈,搜索逐渐向外扩展。在非负边权条件下,当某个节点被正确地确定为当前最小距离节点时,它的最短距离就能够最终确定。这个过程形成了可靠的最短路径算法。

为什么它看起来没有方向感?

假设目标G位于起点S的正东方向。但S的西边,也有许多道路。如果西边的节点距离S很近,迪杰斯特拉算法可能先探索它们。因为它主要比较的是:g(n)\boxed{g(n)}也就是:从起点到当前节点,已经找到的最小成本。它并不把“当前节点距离目标还有多远”作为选择扩展顺序的一部分。因此,它可能探索一些与目标所在方向明显不同的区域。这不是算法错误。相反,它是算法设计目标带来的自然结果。迪杰斯特拉算法需要保证最短路径。但它不会凭空认为某个方向更值得优先探索。

如果我们给它加入“距离目标的估计”呢?

这正是A*的突破。假设机器人已经走到节点 nn。它知道自己从起点来到这里花费了:g(n)g(n)但如果还知道从当前节点到终点的估计距离:h(n)h(n)那么它就可以计算:f(n)=g(n)+h(n)\boxed{f(n)=g(n)+h(n)}并优先探索 f(n)f(n) 较小的候选节点。我们可以将它理解为:“目前已经支付的成本”,加上“估计还要支付的成本”。这就像旅行者不再只查看已经走了多少公里,还会考虑最终可能需要走多少公里。但是,这里隐藏着一个非常大的风险。如果我们估计错误,会发生什么?如果某条道路虽然看似靠近目标,却通向死胡同呢?如果某条看似远离目标的道路,才是最终最短的绕行路线呢?如果启发式函数严重高估某个方向的代价,搜索是否会错过真正最短的路线?所以,简单地把两种数字加起来,并不能自动得到一个正确算法。我们还需要数学理论,解释哪些估计是安全的。1968年的Hart、Nilsson与Raphael,正是围绕这个问题展开研究。

第三幕:1968年,那篇真正定义A*的论文

1968年《IEEE Transactions on Systems Science and Cybernetics》原始论文,第100—107页。首页记录三位作者来自斯坦福研究所人工智能研究组,论文于1967年11月24日收到。[数字对象标识符][ai.stanford.edu]如果你打开1968年的原始论文,会发现它并不是一本教机器人怎样避障的操作手册。论文首先讨论的是一个更普遍的科学问题。此前,人们研究图搜索,存在两种比较明显的方向。一种强调数学保证。算法必须证明能够找到最低成本路径。另一种强调启发式知识。例如,在某些游戏或几何证明问题中,人们会使用关于问题本身的特殊知识,减少探索。但这些启发式规则,往往缺少统一的理论框架。三位作者希望把两者结合起来。既允许计算机使用与问题有关的估计信息。又能够分析什么时候这种搜索依然可靠。他们在论文中定义了一种算法:A*。论文使用带帽符号来区分估计值与真实值,核心思想与现代写法完全一致:f(n)=g(n)+h(n)\boxed{f(n)=g(n)+h(n)}不过,这里有一项值得准确理解的技术细节。论文里的真实最短距离、算法当前已发现的路径成本,以及启发式估计,是不同的对象。现代教材经常简写:g(n)=从起点到n的当前最佳已知成本g(n)=\text{从起点到n的当前最佳已知成本}h(n)=从n到目标的预计剩余成本h(n)=\text{从n到目标的预计剩余成本}f(n)=g(n)+h(n)f(n)=g(n)+h(n)我们本章也将使用这套更容易编程的记法。

先举一个没有障碍物的例子

假设机器人所在位置是:S=(0,0)S=(0,0)目标:G=(6,4)G=(6,4)机器人只允许向上、下、左、右移动。每次移动一格,成本为1。那么,不考虑障碍物,从S抵达G至少需要:向右6格。向上4格。所以最少需要:6+4=106+4=10步。这一项估计,叫作曼哈顿距离(Manhattan Distance)。其公式是:h(n)=∣xn−xG∣+∣yn−yG∣\boxed{ h(n)=|x_n-x_G|+|y_n-y_G| }如果当前节点已经走了3步:g(n)=3g(n)=3而它距离目标的曼哈顿距离是7:h(n)=7h(n)=7那么:f(n)=3+7=10f(n)=3+7=10再考虑另一条候选路径。机器人已经走了5步。但当前位置的曼哈顿距离只剩下4:g(m)=5g(m)=5h(m)=4h(m)=4所以:f(m)=9f(m)=9此时,A*会优先考虑 mm,而不是 nn。因为当前信息表明:经过 mm 抵达目标,可能只需要9个成本单位。而经过 nn 的估计总成本是10。

一张表,理解三种搜索算法

搜索方法优先考虑主要特点
迪杰斯特拉算法g(n)g(n) 最小重视已经走过的成本
贪心最佳优先搜索h(n)h(n) 最小重视看起来最接近目标
A*g(n)+h(n)g(n)+h(n) 最小同时考虑已付成本与剩余估计
这三种方法具有紧密的关系。如果我们让A*中的:h(n)=0h(n)=0那么:f(n)=g(n)f(n)=g(n)在相应实现下,A*就退化为迪杰斯特拉式搜索。而如果只根据:h(n)h(n)来选择下一个扩展对象,那么搜索会变得更像贪心最佳优先算法。它可能很快找到目标。但未必能够找到最短路线。A*并不是简单地在“慢但正确”和“快但可能错误”之间取一个平均值。真正重要的是:它用 gg 保留已经发生的事实,用 hh 引导未来的搜索。同时,借助对启发式函数的数学限制,在特定条件下维持最优性保证。

OPEN 与 CLOSED:两份名单

原始论文还明确使用了两类节点状态。**OPEN:**已经发现,但还没有扩展的节点。**CLOSED:**已经被扩展处理的节点。在现代实现中,我们经常使用优先队列管理OPEN。因为需要反复选择:f(n)f(n)最小的候选节点。当节点被扩展时,程序检查它可以到达的邻居。如果发现一条更便宜的路径,就更新对应邻居的:gg和:ff如果启发式函数和具体实现满足相应条件,搜索最终就可以得到最优路径。这套结构,与前面学过的扫描线算法有一个很有趣的共同点。扫描线算法通过优先队列,选择下一个需要处理的几何事件。A*通过优先队列,选择下一个最值得扩展的搜索状态。它们不是同一种算法。但都把一种复杂过程,组织成了对候选对象的优先选择。

为什么A*这个名称如此特别?

很多算法直接使用发明者的名字。迪杰斯特拉算法。Graham Scan。Bentley–Ottmann算法。但A*采用了一个非常简短的符号。在1968年的论文里,作者把一类搜索过程记作A,并将具有特定性能的版本称为A*。星号表示其在所研究的算法体系中具有特殊的最优性质。但这里的“最优”,需要进一步区分:一方面,它可以指找到最低成本路径的正确性。另一方面,论文还研究了在某些比较条件下,算法所需扩展节点数量的效率性质。这两种“最优”并不相同。而关于后一种性质,后来的数学研究还进一步修正与细化了原始结论。我们将在第八幕回到这段重要的科学史。现在,需要先从最容易理解的第一项保证开始。怎样确保A*不会被自己的方向感欺骗?

第四幕:可以估计,但不能乱猜——可采纳启发式

从当前位置到目标的直线距离,通常可以作为路径长度的一项下界。但真实路线需要绕开障碍物,因此实际代价可能更高。假设一台机器人位于迷宫左侧。目标就在它的右边。如果不考虑障碍物,两点之间的直线距离可能只有:88个单位。可是中间存在墙壁。机器人必须绕行。最终最短路线可能长达:1414个单位。于是,我们拥有两个不同的数字。真正还需要付出的最小成本:h∗(n)=14h^*(n)=14算法使用的估计:h(n)=8h(n)=8这里,星号标记的是真实最优剩余成本。而普通的 h(n)h(n) 是启发式估计。虽然估计并不准确,但它有一个重要特点:8≤148\le14它没有高估。这便是A*理论中最重要的概念之一。

可采纳性:永远不要高估剩余成本

如果启发式函数对每个节点都满足:0≤h(n)≤h∗(n)\boxed{0\le h(n)\le h^*(n)}并且目标节点:h(t)=0h(t)=0那么,我们称它为可采纳启发式(Admissible Heuristic)。这里采用非负启发式的常见标准设定;在更一般的形式化讨论中,条件可以按具体模型稍作调整。可采纳性并不是要求估计完全正确。也不要求误差必须很小。它只要求:估计值不能超过真实最优剩余成本。这相当于告诉机器人:“我不能准确预测还有多少路,但可以给你一个有根据的下界。”

为什么下界比随便猜一个数字更有价值?

假设我们从S出发,真正的最短路径总成本是:C∗=20C^*=20搜索过程中,某条候选路线已经花费:g(n)=7g(n)=7而启发式估计:h(n)=10h(n)=10那么:f(n)=7+10=17f(n)=7+10=17如果 hh 是可采纳的,那么17就是经过这条已知路径、继续到达目标的总成本下界。真正完成这条候选路线,可能需要18、20甚至30。但不会少于17。这意味着:如果算法已经找到一条成本为20的完整路径,而其他候选状态的 ff 都不小于20,那么我们就拥有了证明最优性的重要条件。不过,标准A*通常不需要在第一次发现目标时立即停止。它需要遵循正确的优先队列扩展规则,在目标以适当方式被取出时结束。

曼哈顿距离为什么安全?

考虑一张正方形网格。机器人只允许:上。下。左。右。每次移动成本为1。从:n=(x,y)n=(x,y)到:t=(xt,yt)t=(x_t,y_t)无论选择什么路线,机器人至少要完成:∣x−xt∣|x-x_t|次水平移动,以及:∣y−yt∣|y-y_t|次垂直移动。所以:h(n)=∣x−xt∣+∣y−yt∣h(n)=|x-x_t|+|y-y_t|一定不会超过真正的最短路径成本。障碍物只能迫使机器人走更多路,不可能让它以少于必要的水平和垂直步数抵达目标。因此:hManhattan(n)≤h∗(n)\boxed{h_{\text{Manhattan}}(n)\le h^*(n)}它是一种可采纳启发式。
图44-2:有障碍物时,曼哈顿距离仍然是下界
SG绿色:不考虑障碍的距离下界;橙色:绕行示意
查看 SVG 配图原件
示意图原件 44-008-3ce58b2b88b1.svg
44-008-3ce58b2b88b1.svg · 点击查看大图

但曼哈顿距离并不是永远安全

假设机器人允许斜向移动。从:(0,0)(0,0)到:(1,1)(1,1)如果允许一步斜走,并且成本是:2\sqrt2那么真实最短成本就是:h∗=2≈1.414h^*=\sqrt2\approx1.414但曼哈顿距离会给出:h=∣1∣+∣1∣=2h=|1|+|1|=2于是:2>1.4142>1.414它高估了剩余成本。因此,不再可采纳。同样一条启发式公式,在不同的移动规则下,可能从正确变成错误。这不是公式本身的问题。而是因为我们改变了问题模型。如果允许八方向移动,横竖成本为1,斜向成本为 2\sqrt2,一种常用的下界是:hoctile=max⁡(dx,dy)+(2−1)min⁡(dx,dy)\boxed{ h_{\text{octile}} = \max(dx,dy)+(\sqrt2-1)\min(dx,dy) }其中:dx=∣x−xt∣,dy=∣y−yt∣dx=|x-x_t|,\qquad dy=|y-y_t|对于允许八方向移动的欧几里得单位网格,它可以表示无障碍情况下的最小移动成本。如果每次斜向移动成本也为1,则可以使用切比雪夫距离:h(n)=max⁡(dx,dy)\boxed{h(n)=\max(dx,dy)}因此,设计启发式函数时,必须先理解移动规则。

还有一种永远安全,但可能缺乏帮助的估计

我们可以规定:h(n)=0\boxed{h(n)=0}对所有节点都成立。在非负路径成本模型中,真实剩余成本不会小于0。所以,零启发式始终不会高估。它是可采纳的。但它完全没有方向信息。此时:f(n)=g(n)f(n)=g(n)A*便退化为迪杰斯特拉式的搜索。这说明一个重要事实。A*不是因为启发式一定准确才可靠,而是因为启发式在适当条件下提供了安全的成本下界。估计越有信息量,可能越有助于减少探索。但估计绝不能为了看起来聪明,就随意突破正确性所需要的边界。

第五幕:比“不高估”更进一步——一致性为什么重要?

一致性将启发式估计与每条边的真实成本联系起来。它是一项局部条件,能够保证A*的优先级在路径上不会下降。现在,我们已经理解可采纳性。但1968年的研究还有一个更加精细的概念。它叫作:一致性(Consistency)。也常被称为单调性(Monotonicity)。

一个小小的三角不等式

假设当前节点是:uu它可以移动到邻居:vv这次移动的真实成本为:c(u,v)c(u,v)一致性要求:h(u)≤c(u,v)+h(v)\boxed{h(u)\le c(u,v)+h(v)}对每条可用边都成立。同时通常要求:h(t)=0h(t)=0这条式子看起来十分熟悉。因为它很像三角不等式。我们可以把它理解为:从u到目标的估计距离,不应该比“先从u走到v,再从v到目标的估计距离”更大。它要求启发式估计在局部移动中保持逻辑协调。

为什么它能保证 ff 不会下降?

假设算法沿一条边从u移动到v。那么:g(v)=g(u)+c(u,v)g(v)=g(u)+c(u,v)这里暂时考虑沿当前路径扩展得到的成本。因为一致性:h(u)≤c(u,v)+h(v)h(u)\le c(u,v)+h(v)所以:g(u)+h(u)≤g(u)+c(u,v)+h(v)g(u)+h(u) \le g(u)+c(u,v)+h(v)也就是:f(u)≤f(v)\boxed{f(u)\le f(v)}这说明,沿着同一条路径,估计总成本:ff不会突然下降。对于A*而言,这是一个非常有用的性质。它意味着,搜索过程中节点的优先级不会因为沿路径向前移动而出现违背一致性的突然改善。

可采纳与一致性,有什么区别?

可采纳要求:h(n)≤h∗(n)h(n)\le h^*(n)它比较的是:当前估计与真正的最优剩余成本。一致性要求:h(u)≤c(u,v)+h(v)h(u)\le c(u,v)+h(v)它比较的是:相邻节点之间的局部估计关系。在常见的有限图、非负边权、目标启发值为零的设定中:一致性⇒可采纳性\boxed{\text{一致性}\Rightarrow\text{可采纳性}}证明并不复杂。假设从节点n到目标的最短路径是:n=v0→v1→⋯→vk=tn=v_0\rightarrow v_1\rightarrow\cdots\rightarrow v_k=t沿着路径反复应用一致性:h(v0)≤c(v0,v1)+h(v1)h(v_0)\le c(v_0,v_1)+h(v_1)h(v1)≤c(v1,v2)+h(v2)h(v_1)\le c(v_1,v_2)+h(v_2)逐项连接,得到:h(n)≤∑i=0k−1c(vi,vi+1)+h(t)h(n)\le\sum_{i=0}^{k-1}c(v_i,v_{i+1})+h(t)由于:h(t)=0h(t)=0所以:h(n)≤h∗(n)h(n)\le h^*(n)于是,一致性保证了可采纳性。但反过来:可采纳性⇏一致性\text{可采纳性}\not\Rightarrow\text{一致性}并不是所有不会高估的启发式,都拥有良好的局部一致性。

为什么这对实际程序如此重要?

在图搜索中,多个不同路径可能抵达同一个节点。假设节点A已经被扩展,并被放入CLOSED集合。后来,算法发现另一条更便宜的路径也可以到达A。应该怎么办?如果启发式是一致的,并且程序使用正确的标准A*规则,那么一个节点第一次以最佳优先级被正式扩展时,它的最短到达成本已经确定。因此,之后不需要为了更低的 gg 值重新打开该节点。这使实现更加简单。但如果启发式只是可采纳,却不一致,就可能出现:节点A已经被关闭。后来却发现一条更短的路径能够到达A。这时,如果程序完全禁止重新打开A,就可能得到错误答案。因此:对于允许不一致但可采纳启发式的一般图搜索实现,必须正确处理已关闭节点的重新打开问题。这一点在人工智能搜索的历史中,曾经得到进一步研究和修正。[IJCAI][南卡罗来纳大学计算机科学与工程系]

一个容易记住的区别

可采纳性关心:
我有没有把未来的成本估得过高?
一致性关心:
我对相邻位置的估计,是否遵守局部成本关系?
第一项帮助保证最终解的正确性。第二项帮助让图搜索过程更加稳定,并支持更简单的CLOSED处理方式。这两个概念相似,却不能混为一谈。

第六幕:方向感太强,反而可能把机器人带进错误的道路

启发式搜索的重要风险:看起来更接近目标的方向,不一定构成最低成本路线。缺乏安全下界保证的估计,可能让算法过早接受次优解。现在,我们终于要研究A*最容易遭遇的失败。而且,我们不使用复杂地图。只需要四个节点。S,A,B,GS,\quad A,\quad B,\quad G其中S是起点。G是目标。道路如下:S→A,c=2S\rightarrow A,\quad c=2A→G,c=2A\rightarrow G,\quad c=2S→B,c=1S\rightarrow B,\quad c=1B→G,c=7B\rightarrow G,\quad c=7
图44-3:一张只有两条完整路线的图
SABG2217绿色总成本4;橙色总成本8
查看 SVG 配图原件
示意图原件 44-011-2a6c385600db.svg
44-011-2a6c385600db.svg · 点击查看大图
真正最短的路线显然是:S→A→GS\rightarrow A\rightarrow G总成本:2+2=42+2=4而另一条:S→B→GS\rightarrow B\rightarrow G总成本:1+7=81+7=8

如果启发式故意高估A呢?

假设我们使用一套不合理的估计:h(A)=100h(A)=100h(B)=0h(B)=0h(G)=0h(G)=0当S被扩展后:对A:g(A)=2g(A)=2f(A)=2+100=102f(A)=2+100=102对B:g(B)=1g(B)=1f(B)=1+0=1f(B)=1+0=1算法自然会优先扩展B。然后发现:B→GB\rightarrow G总成本为8。此时,G的:f(G)=8f(G)=8而A的:f(A)=102f(A)=102如果按照标准A*的优先队列规则,目标G会先于A出队。算法于是返回成本8的路线。但真正最优是4。算法失败了。为什么?因为:h(A)=100h(A)=100远远超过A到目标的真实最短成本:h∗(A)=2h^*(A)=2它不是可采纳启发式。这个例子说明:只要我们随意夸大某些节点的未来成本,就可能让算法过早排斥真正有价值的路线。

如果启发式只是低估,但不一致呢?

现在考虑另一张图。边的成本如下:S→A=3S\rightarrow A=3S→B=1S\rightarrow B=1B→A=1B\rightarrow A=1A→G=2A\rightarrow G=2真正最短路线是:S→B→A→GS\rightarrow B\rightarrow A\rightarrow G总成本:1+1+2=41+1+2=4但如果启发式设为:h(A)=0h(A)=0h(B)=3h(B)=3h(S)=4h(S)=4h(G)=0h(G)=0它仍然是可采纳的。因为A到目标的真实成本是2。B到目标的真实成本是3。S到目标的真实最短成本是4。这些估计都没有高估。可是,它并不一致。对于边:B→AB\rightarrow A成本为1。一致性要求:h(B)≤1+h(A)h(B)\le1+h(A)实际却是:3≤1+03\le1+0显然不成立。

不允许重新打开节点,会发生什么?

从S出发,首先发现:g(A)=3,f(A)=3g(A)=3,\quad f(A)=3以及:g(B)=1,f(B)=4g(B)=1,\quad f(B)=4A会先被扩展。程序得到:g(G)=5g(G)=5然后,B被扩展。它发现一条更短的路径:S→B→AS\rightarrow B\rightarrow A使:g(A)=2g(A)=2如果程序已经把A永久关闭,并且不允许重新打开,那么这项改进可能无法继续传播到G。最终程序可能返回成本5的路线,而不是成本4的最优路线。但是,如果正确地重新打开A,算法就能更新:g(G)=4g(G)=4从而恢复最优解。
同样是启发式搜索,区别可能非常大
条件关键结论
随意高估 hh一般不能保证最短路径
可采纳但不一致正确实现需允许必要的节点重开
一致且非负的启发式标准A*图搜索可安全地将已确定节点保持关闭
这也解释了为什么,A*不是一项“按照公式排序”就能完整正确的算法。真正的A*还必须正确管理:节点的最优已知成本。优先队列。父节点。重复状态。改进路径。以及终止条件。数学保证属于完整算法与其前提,而不是单独属于:f=g+hf=g+h这三个字母。

第七幕:互动实验室——让A*亲自穿过迷宫

A*的搜索过程可以可视化为不断扩展的网格区域。探索范围不仅由障碍物决定,还与启发式函数和优先队列的选择规则有关。现在,我们来完成本章最重要的一次实验。我们有一张网格地图。蓝色方格是起点S。绿色方格是目标G。深色方格是障碍物。机器人只能上下左右移动,每一步成本为1。你可以选择四种搜索策略:
  • Dijkstra:只考虑已经花费的成本 gg。
  • A*:根据 g+hg+h 选择节点,其中 hh 使用曼哈顿距离。
  • 贪心搜索:只看剩余距离估计 hh,忽略此前走过的成本。
  • 加权A*:使用 g+2hg+2h;更重视方向,但不再拥有普通A*的通用最优性保证。
实验提供三种地图。一张没有障碍物。一张有墙壁、需要绕路。另一张包含多个障碍区域。请注意观察两件事:搜索算法最终走出的路线。以及它为了证明或发现这条路线,究竟探索了多少个格子。

实验一:逐步观察搜索

图44-4:A*路径搜索实验室网格使用实际的优先级搜索与父节点回溯;移动成本均为1。所有结果会随着地图和算法变化而重新计算。
0 / 25
SG蓝色S:起点 绿色G:终点
已扩展
待扩展OPEN
当前节点
最终路径
准备开始起点已经进入OPEN集合。点击“下一步”,算法将选择优先级最低的节点扩展。
已经扩展的节点数0

四种算法在当前地图上的完整表现
算法扩展节点数路线长度
Dijkstra8213
A*2513
贪心1813
加权A*2013
这些数据仅对应当前小地图与所采用的确定性平局处理规则。不同地图、启发式、数据结构和平局规则,都可能改变扩展节点数量。

实验观察一:没有障碍物时,谁更快?

选择“无障碍”。让Dijkstra完成搜索。然后切换到A*。两者通常都会找到最短路径。但它们扩展的节点分布可能非常不同。Dijkstra更像一圈圈向外寻找。A*则会利用曼哈顿距离,让搜索更倾向于连接起点与目标的区域。这里还需要理解一个容易被忽略的细节。在没有障碍物的四方向网格上,如果一个节点位于某条最短路径对应的矩形区域中,那么:g(n)+h(n)g(n)+h(n)可能都等于真正的最短路线长度。因此,大量候选节点会拥有相同的 ff 值。A*怎样处理这些平局,会显著影响扩展数量。这也是为什么我们不能简单地说:“A*在所有地图上一定比Dijkstra扩展更少的节点。”具体实现和启发式信息量,同样重要。

实验观察二:一堵墙会怎样误导搜索?

选择“绕过墙壁”。机器人与目标几乎位于同一水平线上。曼哈顿距离会鼓励算法朝目标方向移动。但中间有一堵墙。机器人必须绕到上方或者下方的缺口。这时你会发现:A*并没有完全放弃探索。因为它仍然需要判断哪条绕行路径具有最低总成本。它有方向感,但不能只凭方向感作决定。

实验观察三:加权A*与贪心搜索会不会走错?

切换到“贪心”和“加权A*”。你可能看到搜索更集中。但也可能看到最终路线变长。这里的核心区别是:标准A*使用可采纳启发式时,可以在对应条件下保证最优性。贪心搜索不提供同样的最优保证。而使用:f(n)=g(n)+2h(n)f(n)=g(n)+2h(n)的加权A*,已经不再满足经典普通A*的同一组最优性条件。在某些模型与额外假设下,加权A可以拥有另外的次优性界,但不能将它们与标准A的最优性结论混为一谈。这项实验就是为了让你感受到:搜索变得更有方向,不等于最终路线一定更短。

第八幕:1972年的更正与1985年的反驳——经典算法也需要不断修订

尼尔斯·尼尔森(Nils J. Nilsson,1933—2019)。他长期从事搜索、规划、机器学习及机器人研究,曾担任斯坦福大学计算机科学系主任。[Stanford University School of Engineering]我们已经知道:A*能够在合适的条件下找到最优路径。但如果只把1968年的论文视为一项从诞生起就拥有全部最终理论结论的完美成果,就会错过一段非常重要的科学史。事实上,关于A*的正确性、效率与启发式性质,研究者在后来几十年里持续进行了讨论。其中有两个特别值得记住的年份:1972年。1985年。

1972年:三位作者主动修订自己的理论证明

1968年的论文,研究了A*的两项不同性质。第一项是解的最优性。即在相应条件下,A*能够找到真正的最低成本路径。第二项则是搜索效率意义上的最优性。作者希望说明,与某些可比较的搜索算法相比,A*不会扩展不必要的节点。在原始分析中,一致性条件被用于证明部分重要结论。但后来,一位来自加州州立大学富勒顿分校的教授 Ron Colman 发现:对于其中一些主定理,原来的证明似乎使用了比真正需要的更强的条件。他将这一观察告诉了作者。1972年12月,Hart、Nilsson与Raphael 在《ACM SIGART Bulletin》第37期发表了一篇两页的更正:Correction to “A Formal Basis for the Heuristic Determination of Minimum Cost Paths”。他们承认,在有关搜索效率的部分论证里,可以使用更弱的条件,而不必完全依赖最初所假设的一致性。[CiNii Research][南卡罗来纳大学计算机科学与工程系]
图44-5:1968年原论文与1972年更正

1968

提出A*及其形式化理论研究最低成本路径、启发式下界、可采纳性,以及搜索效率的理论性质。

1972

根据其他研究者的观察修订证明作者重新分析一致性假设在若干定理中的作用,同时指出一致性依然有助于避免重新打开已关闭节点。
这段历史里有一件特别值得尊重的事情。作者并没有因为自己的论文已经成为经典,就拒绝重新检查证明。他们公开承认,原来的论证还可以加强。同时保留一致性在实现与部分引理中的实际价值。对于算法理论而言,这恰恰是科学严谨的表现。但是,研究故事并没有在1972年结束。

1985年:一篇论文重新审查“最优效率”

1985年,里娜·德赫特(Rina Dechter)与 朱迪亚·珀尔(Judea Pearl)在《Journal of the ACM》发表:Generalized Best-First Search Strategies and the Optimality of A*。《广义最佳优先搜索策略与A*的最优性》。这项研究重新分析了A*在搜索效率意义上的最优性。他们指出:如果只要求启发式可采纳,但不要求一致性,那么A*并不具有此前某些表述中那样广泛的搜索效率最优性。在特定的算法类别和比较标准下,甚至不存在单一算法能够满足所提出的全部最优效率要求。但在对启发式一致性与比较对象施加适当限制之后,A*的相关最优效率结论仍然成立。[Journal of the ACM]这件事容易被误解。它并不是说:“A*在1985年被证明不能找最短路径。”也不是说:“1968年的整个A*算法被推翻了。”真正被进一步细化的,是另一项更加微妙的性质:在什么条件下,A*扩展节点的数量,能够被认为在某类搜索算法中具有最优效率?它与“最终路线是不是最短的”不是同一个问题。

三种经常被混淆的保证

**图44-6:A*的三种不同性质**
解的最优性是否能够找到从起点到目标的最低成本路径?

搜索效率在指定的算法类别、知识条件与比较标准下,需要扩展多少节点?

实际运行表现在某台计算机上,使用具体数据结构、启发式和地图,需要多少时间与内存?
这三种性质之间有关联,却不能混为一谈。A*可以找到最优路线,但仍然需要扩展大量节点。某种搜索方法可以扩展较少节点,却因为单次运算成本更高,实际运行时间并不一定更短。而某种快速方法,也可能因为启发式设计不当,失去最短路径保证。因此,不能简单地把A*称为:“在所有情况下都最快的最短路径算法。”更准确的说法是:A*是一类利用启发式下界指导搜索、并在相应数学条件与正确实现下保证最低成本解的经典算法。它的实际效率高度依赖启发式质量、图结构、平局处理与数据结构。

一位人工智能先驱的后半生

尼尔森后来从斯坦福研究所转入斯坦福大学。他继续研究人工智能,并撰写多部重要著作,包括2010年的:The Quest for Artificial Intelligence: A History of Ideas and Achievements《人工智能探索:思想与成就的历史》。他于2019年4月23日去世,享年86岁。斯坦福大学在纪念文章中,特别提到他在 Shakey、STRIPS 和 A* 等研究中的贡献。[Stanford University School of Engineering]从1968年的A*论文,到几十年后的机器人与智能系统,尼尔森参与的这些研究逐渐成为人工智能历史的重要组成部分。但我们也应该记住:A*不是一个人独自完成的成果。Hart、Nilsson和Raphael共同署名了原始论文。Shakey 背后还有 Charles Rosen 等大批工程师与研究人员。后来的学者又不断检验、修订和扩展其理论。这与我们前面很多章看到的科学史完全一致。算法的诞生,是一个时刻;算法的成熟,则可能需要几代研究者。

第九幕:亲手实现A*——让每一步都能够解释

现代A*通常使用优先队列、最佳已知成本表和父节点记录来执行搜索。下列程序把这些数据结构分开实现,便于检查算法为何选择某个节点。现在,我们要把1968年的核心思想完整写成代码。这一次不调用任何专业路径规划库。只使用 Python 标准库。我们的地图由以下符号组成:S:起点。G:目标。.:可通行位置。#:障碍物。每一步只允许向上、下、左、右移动。移动成本固定为1。因此,可以使用曼哈顿距离作为一致且可采纳的启发式。不过,我们的程序还会保留重新打开节点的能力。这样,当未来更换成某种可采纳但不一致的启发式时,仍然能够正确处理发现更低路径成本的情况。

先理解三张表

A*至少需要维护三类重要信息。第一张表:g_score保存目前已发现的、从起点到各个节点的最低路径成本。第二张表:came_from记录每个节点的当前最佳前驱节点。这样在搜索找到目标之后,我们就可以从目标一路倒推回起点。第三项结构:open_heap保存已经发现、等待扩展的候选节点。每次选择:f(n)=g(n)+h(n)f(n)=g(n)+h(n)最小的节点。

完整 Python 程序

python

import heapq
import itertools


GRID = [
    "S...#....",
    ".##.#.##.",
    "...#.....",
    ".#...#...",
    "...#....G",
]


def locate(grid, symbol):
    for y, row in enumerate(grid):
        for x, cell in enumerate(row):
            if cell == symbol:
                return (x, y)
    raise ValueError(f"找不到 {symbol}")


def astar(grid, heuristic="manhattan"):
    """
    网格 A* 搜索。

    - 只允许上下左右移动
    - 每次移动成本为1
    - 支持 h=0 或曼哈顿距离
    - 使用懒删除处理优先队列旧条目
    - 改善已关闭节点时允许重新打开

    返回:路径、成本、扩展节点数
    """

    width = len(grid[0])

    if any(len(row) != width for row in grid):
        raise ValueError("网格每行长度必须相同")

    height = len(grid)

    start = locate(grid, "S")
    goal = locate(grid, "G")

    def h(node):
        if heuristic == "zero":
            return 0

        if heuristic == "manhattan":
            x, y = node
            gx, gy = goal

            return abs(x - gx) + abs(y - gy)

        raise ValueError("未知启发式")

    def neighbors(node):
        x, y = node

        for dx, dy in [
            (1, 0),
            (0, 1),
            (-1, 0),
            (0, -1),
        ]:
            nx, ny = x + dx, y + dy

            if not (0 <= nx < width):
                continue

            if not (0 <= ny < height):
                continue

            if grid[ny][nx] == "#":
                continue

            yield (nx, ny)

    counter = itertools.count()

    open_heap = []

    # 堆元素为:(f, h, 顺序号, g, 节点)
    heapq.heappush(
        open_heap,
        (h(start), h(start), next(counter), 0, start)
    )

    g_score = {start: 0}
    came_from = {}

    closed = set()
    expansions = 0

    while open_heap:
        f, h_value, _, g, current = heapq.heappop(
            open_heap
        )

        # 堆中可能保留旧的、已经过时的记录
        if g != g_score.get(current):
            continue

        if current in closed:
            continue

        closed.add(current)
        expansions += 1

        # 目标被正式取出时才结束
        if current == goal:
            path = [goal]

            while path[-1] != start:
                path.append(came_from[path[-1]])

            path.reverse()

            return {
                "path": path,
                "cost": g_score[goal],
                "expanded": expansions
            }

        for nxt in neighbors(current):
            tentative = g_score[current] + 1

            if tentative >= g_score.get(
                nxt, float("inf")
            ):
                continue

            # 发现一条更便宜的路径
            g_score[nxt] = tentative
            came_from[nxt] = current

            # 必要时重新打开节点
            closed.discard(nxt)

            hn = h(nxt)
            fn = tentative + hn

            heapq.heappush(
                open_heap,
                (
                    fn,
                    hn,
                    next(counter),
                    tentative,
                    nxt
                )
            )

    return {
        "path": None,
        "cost": None,
        "expanded": expansions
    }


def draw_path(grid, path):
    if path is None:
        return "\n".join(grid)

    canvas = [list(row) for row in grid]

    for x, y in path[1:-1]:
        canvas[y][x] = "*"

    return "\n".join(
        "".join(row) for row in canvas
    )


astar_result = astar(GRID, "manhattan")
dijkstra_result = astar(GRID, "zero")

print("A*路径成本:", astar_result["cost"])
print("A*扩展节点:", astar_result["expanded"])

print(
    "Dijkstra路径成本:",
    dijkstra_result["cost"]
)

print(
    "Dijkstra扩展节点:",
    dijkstra_result["expanded"]
)

print(draw_path(GRID, astar_result["path"]))

assert astar_result["cost"] == dijkstra_result["cost"]
对于这张地图,真正的最低路径成本是:12\boxed{12}因此,两个算法应该得到相同的最低成本。但路径本身可能不完全相同。如果存在多条同样短的路线,A*和Dijkstra可能因为优先级与平局处理方式不同,返回不同的最短路径。这是正确的。算法要求最小成本。它并不一定要求所有实现都输出同一条路线。

为什么需要保留优先队列中的旧记录?

观察这段代码:
python
if g != g_score.get(current):
    continue
假设某个节点最初被发现时:g(n)=10g(n)=10于是堆中加入一条记录。后来,算法找到一条更短的路径:g(n)=7g(n)=7新记录会再次进入优先队列。但是,原来成本为10的旧记录还可能留在堆中。当它被弹出时,发现:10≠710\ne7于是跳过。这种做法叫作惰性删除(Lazy Deletion)。它避免了在普通二叉堆中直接修改任意已有元素优先级的麻烦。在 Python 中,这是一种常见而实用的优先队列实现方式。

为什么要保存父节点?

假设算法最后到达目标G。此时我们已经知道:g(G)=12g(G)=12但这只是路线长度。如果机器人需要真正移动,就还必须知道路径经过哪些格子。因此,我们保存:
python
came_from[next_node] = current
每当发现一条更好的路径,就更新当前节点的前驱。到达目标之后,从G出发沿着父节点不断回溯。最终便得到:G→⋯→SG\rightarrow\cdots\rightarrow S再将结果反转,就成为机器人应该执行的路线:S→⋯→GS\rightarrow\cdots\rightarrow G这是一项非常简单,却不可缺少的工程细节。没有父节点记录,我们可能只知道最低成本是多少,却无法直接恢复具体路线。

代码里还有一个值得学习的优化

我们在堆中保存:
python
(f, h, 顺序号, g, 节点)
为什么不只保存:
python
(f, 节点)
因为多个节点可能拥有相同的:ff为了让搜索过程具有稳定、可重复的选择规则,我们进一步比较:hh也就是,在 ff 相同的情况下,优先考虑估计更接近目标的节点。如果两者仍相同,再使用递增顺序号,保证排序稳定。这是一种平局处理策略。但并不是A*唯一合法的平局规则。不同规则可能改变扩展节点的数量。在具有大量相同 ff 值的网格地图中,这种影响尤其明显。

如果启发式很差,会不会比Dijkstra更慢?

有可能。A*需要计算启发式函数。还要维护优先队列。如果启发式信息量很弱,或者计算成本本身很高,那么它可能没有节省多少节点扩展,却增加了额外的计算开销。因此,使用A*时不能只问:“它是不是比Dijkstra更先进?”还应该问:我的启发式计算有多贵?它能减少多少节点扩展?图是稀疏还是稠密?是否存在大量相同优先级?需要查询一次路线,还是重复查询很多次?对于不同问题,最合适的算法与数据结构可能不同。这再次提醒我们:一项算法的理论正确性保证,与它在具体工程中的实际性能,是两类需要分别评估的问题。

第十幕:从 Shakey 到现代机器人,A*怎样真正进入物理世界?

现代移动机器人需要将地图、障碍物、运动约束和路径规划结合起来。A*及其变体是相关规划系统中的重要算法家族。1968年,Shakey 面对的是一组经过人工设计的实验室房间。研究人员需要借助远程大型计算机,帮助它处理环境感知和行动规划。半个多世纪后,许多机器人已经能够在机载计算机上执行复杂算法。它们可以使用:摄像头。激光雷达。深度传感器。地图与定位系统。以及多种搜索与优化算法。但无论硬件怎样变化,路径规划依然需要回答同一个问题:在已知或部分已知的环境中,怎样找到一条满足约束、成本足够低的路线?

应用一:电子游戏中的角色寻路

电子游戏通常需要让角色在地图中寻找道路。A*的状态可以是网格、导航图节点或其他离散化的可通行位置。想象一个策略游戏。你在地图的一角,给士兵指定一个目标位置。士兵需要绕过房屋、围墙和树木,最终走到目的地。如果地图能够表示为一张可通行网格,那么我们可以直接使用A*。但真实游戏也经常使用导航网格(Navigation Mesh)。它不是把每一个位置都切成正方形。而是用多边形区域描述可行走空间。在这些区域之间,可以建立相邻关系。然后搜索从起点所在区域到目标区域的通道。最后再通过路径平滑等方法,生成适合角色移动的路线。这里需要注意:A*处理的是图中的状态与边。它不要求这些节点一定来自正方形网格。因此,A*的真正研究对象是图搜索,而不是某一种特定地图画法。

应用二:机器人路径规划

现代ROS 2导航系统 Nav2 提供了多种路径规划器。其中的 Smac Planner,包括:二维网格A*。Hybrid-A*。以及状态格(State Lattice)相关规划器。这些工具面向不同类型的机器人和运动模型。[GitHub][Nav2 Docs]为什么需要这么多变体?因为真实机器人不是一个没有体积的数学点。一台汽车不能随意横向移动。它有:车身长度。车身宽度。最小转弯半径。前进和倒车约束。以及碰撞检测需求。如果我们只在二维网格中搜索:(x,y)(x,y)可能会得到一条几何上避开障碍,却要求汽车突然原地横转90度的路线。这样的路线对于普通汽车不可执行。于是,研究人员把搜索状态扩展为:(x,y,θ)\boxed{(x,y,\theta)}其中:θ\theta表示车身朝向。这样,搜索不再只考虑位置。还考虑姿态。
图44-7:网格A*与车辆运动规划的差别
二维网格数学点可以在格子转角处直接改变方向
考虑车辆姿态路径需要符合转向半径与运动方式
仅为概念对比。实际 Hybrid-A* 使用与车辆运动学相适应的运动基元和碰撞检测,不能简单理解为给网格折线画一个圆角。
这就是 Hybrid-A* 等方法的重要动机。它们将启发式搜索与机器人运动学、姿态状态和可行轨迹生成结合起来。但也必须准确区分:经典A*在离散图上的最优性保证,不会自动变成连续车辆运动规划中所有现实目标的全局最优性保证。我们需要检查:状态空间如何离散化?运动原语是否足够完整?边权是否准确表示实际代价?启发式是否可采纳?碰撞检测是否可靠?这些条件决定了理论保证究竟适用于什么模型。

应用三:地图导航与真实交通

如果我们打开一张道路地图,就会发现它天然可以表示为图。道路交叉口是节点。道路连接是边。边权可能代表:长度。预计通行时间。通行费用。或者某种综合成本。如果我们希望计算从S到G的最短路线,A*可以利用空间坐标构造启发式。例如,当边权代表距离时,可以考虑使用目标之间的直线距离下界。但如果边权代表时间,启发式也必须转换成时间单位。一种具有适当条件的下界是:h(n)=直线距离(n,G)vmax⁡h(n)=\frac{\text{直线距离}(n,G)}{v_{\max}}其中:vmax⁡v_{\max}必须是不低于模型中可能行驶速度的合法上界。只有这样,估计时间才不会超过真实最短通行时间。如果随便使用某条道路的平均速度,就可能高估或低估剩余时间。其中高估会破坏可采纳性。另外,真实交通路网的边权可能随时间变化。早上八点经过某条道路,与晚上十点经过同一条道路,耗时可能不同。此时需要使用适合时间依赖网络的路径规划模型,不能把普通静态图A*的结论不加条件地套用。

应用四:当地图会改变

Shakey 的实验环境相对可控。但现实机器人可能遇到:突然出现的行人。临时放置的箱子。关闭的通道。移动的车辆。这些变化可能让原来的路径失效。一种办法是重新运行A*。但如果环境经常发生局部变化,每次从头搜索可能带来重复计算。因此,研究人员又发展了各种增量式搜索与动态重规划方法。例如:D*。D* Lite。它们与A*存在密切的理论联系,但能够利用之前的搜索信息,处理某些环境变化。这又是一条从经典A*继续演化的研究道路。而它提醒我们:从在固定地图中找到一条路,到在不断变化的世界里持续安全行动,是两个层次不同的问题。

A*为什么没有被更大的语言模型取代?

今天,人工智能系统可以使用大语言模型理解指令。例如:“把仓库东侧的箱子搬到装货区。”语言模型可以协助解释任务。但真正执行时,机器人仍然需要精确处理:哪些位置可通行?哪条运动轨迹不会碰撞?路径成本是多少?当前路线是否仍然可行?这些问题具有明确的几何与算法约束。因此,在现代机器人系统中,大模型与经典规划算法并不必然互相替代。它们可以承担不同职责。语言模型可能参与高层目标理解。图搜索或其他运动规划算法则处理更具体的状态与行动约束。这也解释了为什么1968年的A*,直到今天仍然值得学习。人工智能系统会不断发展。但具有明确数学保证、可以验证行为的基础算法,依然具有长期价值。

第十一幕:A*真正的局限——拥有方向感,并不代表看得见一切

复杂环境中的路径搜索可能包含大量候选状态。即使使用A*,如果启发式信息量不足,仍然可能需要探索很大一部分搜索空间。经过前面十幕,我们已经理解了A*最重要的数学结构。但在本章结束之前,还需要面对它的局限。

第一项局限:最坏情况下,仍然可能搜索大量状态

A*利用启发式函数,让搜索更有方向。但如果启发式函数很弱:h(n)≈0h(n)\approx0它就可能表现得很像迪杰斯特拉算法。如果状态空间非常庞大,依然可能需要扩展大量节点。在显式给定的有限图中,使用一致启发式、合适的优先队列与正确实现,A*可以避免重复扩展已确定的节点,并获得对应的图搜索复杂度界。但在一般隐式状态空间中,能够达到的状态数量可能随问题规模呈指数增长。这就是许多人工智能搜索任务中真正困难的地方。不是某次优先队列操作太慢。而是可能需要考虑的状态实在太多。

第二项局限:内存可能先于计算时间耗尽

经典A*通常需要保存:OPEN候选。已扩展状态。最佳已知成本。父节点关系。如果地图很大,或者搜索状态是:(x,y,θ,v,…)(x,y,\theta,v,\ldots)这样包含多个维度的组合,那么节点数量可能迅速增长。因此,A*可能在尚未完成搜索之前,就已经消耗大量内存。后来,研究人员又发展了利用深度优先搜索与迭代阈值的 IDA* 等方法,尝试在某些问题中减少内存需求。但减少内存也可能增加重复搜索成本。不同方案需要进行权衡。

第三项局限:启发式函数不是免费获得的

一个好的启发式函数,应当具有两项理想性质。第一,能够提供关于剩余成本的有用信息。第二,计算它本身不应该过于昂贵。如果为了得到一个很精确的启发式估计,每个节点都要运行一套昂贵的复杂算法,那么节省的节点扩展,可能被启发式计算成本抵消。因此,实际系统经常在以下因素之间寻找平衡:启发式准确程度。启发式计算成本。搜索节点数量。以及整体运行时间。

第四项局限:离散地图不等于真实物理空间

一台机器人可能拥有一张精确的二维网格。A*在这张网格上找到最短路径。但如果机器人的体积超过一个格子,它可能无法实际通过某条狭窄通道。如果机器人不能原地转向,它也可能无法执行一条包含急转弯的路线。因此,正确的路径规划不能只依赖抽象节点之间的连通性。还必须结合机器人本身的运动约束与碰撞几何。这就是为什么现代机器人研究,会从二维A*继续走向更复杂的状态空间规划。

第五项局限:它不能替我们决定真正应该优化什么

假设有两条路线。第一条长100米。第二条长120米。如果只考虑距离,第一条更好。但第一条可能需要经过一个拥挤、狭窄的通道。第二条虽然更长,却更安全、更容易通行。那么真正的目标究竟是什么?最短距离?最少时间?最低能耗?最小碰撞风险?还是几种目标的综合?A*可以搜索我们定义的成本函数。但不能自动判断哪一种成本函数最符合现实需求。算法能够忠实寻找模型中的最优解,却不能保证模型已经包含现实世界真正重要的一切。

尾声:方向不是答案,而是一种可以被证明安全的希望

让我们再次回到1968年。在门洛帕克的斯坦福研究所,研究人员希望制造一台能够感知、推理并采取行动的机器人。它后来被称为Shakey。这项研究面临的困难,远远不只是如何让轮子转动。研究人员希望机器能够理解任务。构造行动计划。在具有障碍物的空间里找到路线。并将计算结果转化为物理行动。为了实现这些目标,他们发展了一系列人工智能方法。其中,一项研究逐渐成为独立的算法经典。它就是A*。在A*出现之前,迪杰斯特拉等数学家已经为最短路径问题建立了严格方法。它们能够找到最低成本路线。但是,它们不会天然优先朝着目标所在方向探索。而人类在寻找道路时,常常会使用一种非常直观的知识。我们知道目标大概在哪里。我们知道哪些道路看起来更接近目标。我们会根据这种判断,选择先探索哪条路线。但人类的方向感并不总是可靠。我们可能被死胡同欺骗。也可能因为过分相信眼前的距离,而忽略更好的绕行路线。Hart、Nilsson与Raphael 的突破,在于他们没有简单地将这套直觉直接交给计算机。他们将它写成数学公式:f(n)=g(n)+h(n)\boxed{f(n)=g(n)+h(n)}然后认真研究:什么时候,启发式估计不会破坏最短路径保证?什么时候,局部估计之间保持一致?什么时候,可以安全地把节点标记为已经完成?什么时候,需要重新打开之前扩展过的节点?这些研究让算法的方向感,不再只是一种经验技巧。它成为一种可以分析和证明的计算规则。当然,这项理论也不是一开始就拥有最终形态。1972年,原始作者公开修订自己的证明。1985年,Dechter与Pearl进一步研究A*的搜索效率最优性,明确不同结论成立的条件。后来,更多研究者发展了增量搜索、内存受限搜索、加权搜索与机器人运动规划变体。这一发展过程告诉我们:数学保证不是一项算法被发明时附带的永久标签,而是需要始终与具体假设、实现方式和研究结论对应。

从二维空间,到拥有方向的行动

回顾第三篇目前的四章:
《让机器理解空间》· 前四章的递进
第41章
Graham Scan:寻找空间的外边界让计算机根据叉积、极角排序和栈,判断哪些点位于最外围。

第42章
Voronoi Diagram:理解空间的内部归属用距离关系、垂直平分线和半平面交集,构造最近站点区域。

第43章
Plane Sweep:识别空间中的相交事件通过事件队列与动态上下关系,避免检查大量无关的几何对象对。

第44章
A:在空间里朝目标寻找道路*结合已知成本与剩余成本下界,让搜索更有方向,同时保留相应的最优性保证。
我们先让计算机识别边界。再让它理解区域。接着,让它发现几何对象之间的关系。最后,让它能够从一个位置出发,寻找抵达另一个位置的路线。这是一项重要变化。前三章主要研究空间本身。这一章则开始研究:怎样在空间中采取行动。而这正是机器人技术的核心之一。当然,A*还没有解决全部机器人运动问题。它需要一个可搜索的状态空间。需要知道哪些行动合法。需要能够计算行动成本。如果我们面对的不是二维格子,而是一台拥有多关节机械臂的机器人,那么每个状态可能包含大量连续变量。此时,简单地将整个空间划分成规则网格,就可能产生极其庞大的状态数量。所以,下一章我们将再次改变思路。如果无法方便地枚举整个空间,能不能从起点开始,不断向尚未探索的区域伸出新的分支?就像一棵树。慢慢向外生长。不断试探新的空间。直到它找到一条通向目标的道路。这就是另一类重要的机器人运动规划方法。快速探索随机树(Rapidly-exploring Random Tree,RRT)。A*告诉我们:当拥有一张可以搜索的图时,怎样更有方向地寻找最优路线。RRT则将带领我们思考:当状态空间连续而复杂,甚至难以预先建立完整搜索图时,能不能通过采样逐渐探索其中的可行路径?从搜索已知的离散结构,到生长一棵探索连续空间的树。机器人路径规划的故事,还将继续。

历史档案:从荷兰铁路到人工智能搜索

本章使用的主要史实与理论来源如下。
1959年 · Dijkstra 最短路径论文E. W. Dijkstra — A Note on Two Problems in Connexion with Graphs发表于《Numerische Mathematik》第1卷,第269—271页。关于1956年铁路演示的具体历史,另参考迪杰斯特拉1982年的亲自回忆。

1968年 · A 原始论文*Hart、Nilsson、Raphael — A Formal Basis for the Heuristic Determination of Minimum Cost Paths本章最重要的一手文献。发表于《IEEE Transactions on Systems Science and Cybernetics》第4卷第2期,第100—107页。原文明确给出A*、评估函数与相关数学定理。

1966—1972年 · Shakey机器人计算机历史博物馆:Shakey包含机器人照片、研发团队、设备构成,以及A*、STRIPS与早期机器人规划的历史关系。

1972年 · 原始作者的理论更正Correction to A Formal Basis for the Heuristic Determination of Minimum Cost PathsHart、Nilsson与Raphael发表于《SIGART Bulletin》第37期,第28—29页的更正。用于准确区分可采纳性、一致性与搜索效率性质。

1985年 · A*效率最优性的重新研究Rina Dechter & Judea Pearl — Generalized Best-First Search Strategies and the Optimality of A*《Journal of the ACM》第32卷第3期,第505—536页。重要贡献是细化不同算法类别和启发式条件下的搜索效率最优性定义。

2019年 · Nils Nilsson 生平纪念斯坦福大学工程学院:Nils Nilsson, Pioneer in Robotics and AI用于核查尼尔森的教育、Shakey项目、SRI与斯坦福的研究经历及其人工智能史贡献。

现代应用 · ROS 2 Nav2Nav2 Smac Planner 官方实现说明用于说明现代二维A*、Hybrid-A*与状态格规划如何作为机器人导航的可用软件组件。
本章开篇的研究所场景、机器人面对墙壁的具体路线以及图中的节点与迷宫,属于用于解释算法的教学重构,不代表 Shakey 在某年某日执行过完全相同的地图任务。1968年原始论文的作者、发表时间和收稿日期,以及1972年更正和1985年后续研究,均可依据对应文献核查。数学实验以非负、单位移动成本的有限网格为模型。其最优性结论与启发式性质只在相应前提下成立,不应被直接解释为对任意现实交通或连续机器人运动规划的无条件保证。
下一章预告 · 第45章

《让机器人在未知世界中长出一棵树》

1990年代末,美国。机器人运动规划研究者正在面对一个极其棘手的问题。如果机器人只有两个位置坐标,路径规划还比较容易理解。但如果机器人有六个关节呢?如果每个关节都可以连续转动呢?如果每一个状态都由位置、朝向、关节角度和其他运动变量共同决定呢?这时,我们很难提前列出全部可能状态。传统网格搜索可能因为维度增加而迅速变得难以承担。于是,研究者提出了一项截然不同的想法。不再先建立完整的空间搜索图。而是从起点开始,通过随机采样,不断将一棵树的分支伸向新的可行区域。这项研究后来被称为:Rapidly-exploring Random Trees,RRT。下一章,我们将追踪 Steven M. LaValle 在1998年前后的早期研究,以及后来与 James J. Kuffner 共同发展的相关方法。我们将亲手观察一棵搜索树怎样逐步生长,并讨论概率完备性、连续状态空间、碰撞检测,以及为什么“随机探索”并不意味着毫无方向。A*教会机器在一张图上寻找路线;RRT将帮助我们理解,机器怎样在尚未完全离散化的复杂空间里探索可行道路。
——《算法的故事》第二季 · 第44章 完