搜索是符号主义 AI 的发动机:把问题抽象成「状态空间中找一条从初始态到目标态的路」,剩下的是纯算法问题。迷宫、拼图、机器人路径、定理证明、乃至数独,全是同一个抽象。本章按「无信息搜索(只用问题本身)→ 有信息搜索(加上启发式)」的顺序推进,A* 是全章皇冠,也是游戏 AI 和机器人导航的行业标准。
一个搜索问题四件套:
状态空间 = 从初始状态可达的全部状态的图。八数码难题(3×3 拼图)的状态空间 9!/2 = 181440 个状态,棋类大到天文数字——搜索的艺术一半在于「状态空间的定义」:表示得好,空间指数缩小(对称性剪枝、约束传播)。
不借助任何「目标在哪」的知识,只有四种基本策略,各有明确的性能画像:
| 策略 | 数据结构 | 完备性 | 最优性 | 空间 |
|---|---|---|---|---|
| BFS 广度优先 | 队列(数据结构课第 3 章) | 是(分支有限) | 是(等步长) | O(b^d) 可怕 |
| DFS 深度优先 | 栈/递归 | 否(无限深) | 否 | O(bm) 省 |
| 迭代加深 IDS | 反复限深 DFS | 是 | 是(等步长) | O(bd) 兼得 |
| UCS 一致代价 | 优先队列(第 5 章堆) | 是(代价>0) | 是 | O(b^(C*/ε)) |
(b = 分支因子,d = 解的深度,m = 最大深度,C* = 最优代价。)
三个必懂的取舍。BFS 与 DFS 的空间之争:BFS 记住整层(指数内存),DFS 只记一条路径(线性内存)但可能掉进无限深渊;迭代加深是两者的和解:用「DFS 的空间」做出「BFS 的最优性」,代价是重复展开浅层节点(数学上重复的代价可忽略:树状展开时大头永远在最深层)。UCS 的本质:BFS 在「每步代价 1」时的特例,优先队列按路径代价 g(n) 弹出,处理加权图的最优解(数据结构课第 6 章 Dijkstra = UCS 的别名)。
无信息搜索在迷宫里像无头苍蝇;启发式(heuristic)h(n) 是「从 n 到目标的估计代价」——估计,不要钱;准确,要命。好的 h 来自问题的松弛版:八数码的「错位棋子数」(忽略「一次只能移一格」的约束)、「曼哈顿距离」(忽略「不能斜穿」)。
贪心最佳优先:只按 h(n) 扩展,飞快但既不完备也不最优(一头扎向目标,可能撞墙绕远)。
UCS:只按 g(n)(已走代价),最优但瞎(均匀向四周扩散)。
A*:按 f(n) = g(n) + h(n) 扩展——「已花代价 + 预计剩余」,两者取长补短,这是 1968 年 Hart、Nilsson、Raphael 的发明,也是启发式搜索的完成式。
A* 用可采纳的启发式(never overestimate,h(n) ≤ 真实代价)保证找到最优解。证明思路(反证,第 4 章):设最优解代价 C*,但 A* 返回了代价 C > C* 的解;那么在终止前,最优路径上必有未扩展的节点 n,其 f(n) = g(n) + h(n) ≤ C*(可采纳性保证 h 不高估),而被弹出的解节点 f = C > C*,优先队列会先弹 n——矛盾。∎
**一致性(consistency)**是更强的条件:h(n) ≤ cost(n, n') + h(n')(三角不等式),一致则第一次弹出节点时即确定最优(无需重复扩展)。可采纳的 h 越大(越接近真实),剪枝越狠:h = 0 退化为 UCS,h = 真实代价直达答案。设计启发式 = 给问题找一个「松弛版的精确解」:八数码的曼哈顿距离、寻路游戏的对角线距离,全是这个套路。
网格迷宫的完整 A*(Python 伪码):
def astar(grid, start, goal):
open_heap = [(h(start), start)] # (f, 状态) 最小堆,第 5 章的堆
g = {start: 0}
while open_heap:
f, cur = heappop(open_heap)
if cur == goal:
return reconstruct(g, cur) # 回溯路径
for nxt in neighbors(cur): # 四方向
ng = g[cur] + 1 # 每步代价 1
if ng < g.get(nxt, INF):
g[nxt] = ng
heappush(open_heap, (ng + manhattan(nxt, goal), nxt))
曼哈顿距离(|dx| + |dy|)在四向网格上是可采纳的(每步最多靠近一格)。工程变体:游戏地图的 JPS(跳点搜索,A* 的加速版)、机器人上动态更新(重规划 D* lite)、多目标的折中(权重 f = g + εh,牺牲一点最优换速度)。
g 字典就是闭合集。