第03章 通过搜索求解问题 (Solving Problems by Searching)
**摘要:**本章系统阐述了在无感知、确定性、完全可观测、离散、已知环境下的单智能体问题求解方法。核心是将问题形式化为状态空间搜索,并对比了无信息搜索(如 BFS、DFS、UCS、迭代加深)与有信息搜索(如贪心最佳优先、A*)两大类策略。重点介绍了 A* 搜索算法,其结合了实际代价 g(n) 与启发估计 h(n),在启发函数可采纳或一致的条件下能保证找到最优解。此外,本章还探讨了启发式函数的设计方法、各搜索策略的完备性与最优性,以及本章内容与后续章节(如局部搜索、博弈、CSP、规划)的关联,并提出了 A* 与大模型规划结合、搜索爆炸与近似解权衡等延伸思考。
1. 章节概述
本章研究无感知、确定性、完全可观测、离散、已知 环境下的单智能体问题求解,核心是将问题形式化为状态空间搜索,并系统对比无信息与有信息两大类搜索策略。A* 搜索作为连接二者的最优解算法,是本书最重要的算法之一。
2. 关键概念与定义
- 问题形式化(Problem Formulation):用五元组 ⟨状态、初始状态、行动、转移模型、目标测试、路径代价⟩ 描述任务。
- 状态(State) 与 状态空间(State Space):问题所有可能配置的集合;其规模为"分支因子 b × 深度 d"。
- 搜索树(Search Tree):从初始状态展开,节点代表状态(可能重复)。
- 节点扩展(Node Expansion):将节点的后继加入边界(frontier/fringe)。
- 无信息搜索(Uninformed / Blind Search):不使用任何关于目标距离的启发信息。
- 有信息搜索(Informed / Heuristic Search) :利用启发式函数 h(n) 估计到目标的代价。
- 可采纳性(Admissibility):启发式 h(n) 永不高估真实代价(h(n) ≤ h*(n))。
- 一致性(Consistency / Monotonicity):对任意边 (n→n′),h(n) ≤ c(n,n′)+h(n′),且 h(目标)=0。
- 完备性(Completeness) 与 最优性(Optimality):能否找到解 / 能否找到代价最小的路径。
3. 核心理论与算法
问题形式化五元组:
- 初始状态 s_0
- 行动集合 Actions(s)
- 转移模型 Result(s,a)
- 目标测试 Goal?(s)
- 路径代价 c(path)=\sum c(s,a,s')
通用图/树搜索骨架:
pseudocode
function Graph-Search(problem, frontier):
frontier ← INSERT(MAKE-NODE(problem.INITIAL), frontier)
reached ← {problem.INITIAL} # 已访问状态集合(图搜索才有)
loop:
if frontier is empty: return failure
node ← REMOVE-CHOICE(frontier)
if problem.Goal?(node.State): return SOLUTION(node)
for each child in Expand(node):
if child.State not in reached:
reached.add(child.State)
frontier ← INSERT(child, frontier)
return failure
无信息搜索对比:
| 策略 | 数据结构 | 完备性 | 最优性 | 时间/空间 |
|---|---|---|---|---|
| BFS | 队列 FIFO | 是(b有限) | 是(单位代价) | O(b^d) |
| DFS | 栈 LIFO | 否 | 否 | O(bm) |
| UCS(一致代价) | 优先队列(g) | 是 | 是 | O(b^{1+⌊C*/ε⌋}) |
| 深度受限 | 栈+深度限 | 否(限错则无) | 否 | O(b^l) |
| 迭代加深 IDS | 重复受限DFS | 是 | 是(单位代价) | O(b^d) |
| 双向搜索 | 两方向BFS | 是 | 是 | O(b^{d/2}) |
- BFS:逐层扩展,单位步代价下最优,但空间消耗巨大。
- DFS:省空间、可能更快找到深解,但不完备(无限支路)且不最优。
- UCS:以 g(n)(已花费代价)为优先级,是 A* 去掉 h 的特例。
- 迭代加深:兼具 DFS 的空间效率与 BFS 的最优/完备性,是首选的盲目搜索。
- 双向搜索:从初态与目标同时搜,复杂度由 b^d 降为 b^{d/2}。
有信息搜索:
-
贪心最佳优先(Greedy Best-First):按 f(n)=h(n) 排序,快但不完备、不最优。
-
A* 搜索:
<p><strong>原理</strong>:以 f(n)=g(n)+h(n) 为优先级,g(n) 为起点到 n 的实际代价,h(n) 为 n 到目标的启发估计。当 h 可采纳时,A* 在树搜索下保证最优;当 h 一致时,图搜索也最优,且首次扩展某节点即其最优 g 值。</p> <p><strong>公式</strong>: $$f(n)=g(n)+h(n)$ 其中 f(n)$ 估计"经 n 到达目标的总代价"。</p> <p><strong>伪代码</strong>:</p> <pre><code class="language-pseudocode">function A*-Search(problem, h): frontier ← a priority queue ordered by f = g + h frontier.INSERT(MAKE-NODE(problem.INITIAL, g=0)) reached ← {INITIAL: 0} # state → best g loop: if frontier empty: return failure node ← frontier.POP() # 最小 f if problem.Goal?(node.State): return SOLUTION(node) for child in Expand(node): new_g = node.g + cost(node, child) if child.State not in reached or new_g < reached[child.State]: reached[child.State] = new_g child.g = new_g child.f = new_g + h(child) frontier.INSERT(child)</code></pre> <p><strong>适用场景</strong>:路径规划、拼图、最优调度等可建精确启发式的组合问题。 <strong>局限性</strong>:内存消耗大(指数级);h 过小时退化为 UCS;高维连续空间不实用(需第 4 章局部搜索)。</p> </li>
Python 实现示例(罗马尼亚地图问题):
python
import heapq
class Node:
"""搜索节点类"""
def init(self, state, parent=None, action=None, g=0, h=0):
self.state = state # 当前状态(城市名)
self.parent = parent # 父节点
self.action = action # 到达此状态的动作
self.g = g # 从起点到当前节点的实际代价
self.h = h # 启发式估计代价
self.f = g + h # 总估计代价 f(n) = g(n) + h(n)
def __lt__(self, other):
# 用于优先队列比较,按 f 值排序
return self.f < other.f
def a_star_search(start, goal, graph, heuristic):
"""
A* 搜索算法实现
参数:
- start: 起始状态
- goal: 目标状态
- graph: 图结构,dict[state] = [(neighbor, cost), ...]
- heuristic: 启发式函数 heuristic(state) -> 估计代价
返回:
path: 从起点到目标的路径(状态列表)
cost: 路径总代价
"""
# 初始化优先队列(边界)
frontier = []
start_node = Node(start, g=0, h=heuristic(start))
heapq.heappush(frontier, start_node)
# 已访问状态及其最佳 g 值
reached = {start: 0}
while frontier:
# 弹出 f 值最小的节点
current = heapq.heappop(frontier)
# 目标测试
if current.state == goal:
# 重构路径
path = []
node = current
while node:
path.append(node.state)
node = node.parent
return list(reversed(path)), current.g
# 扩展当前节点
for neighbor, step_cost in graph.get(current.state, []):
new_g = current.g + step_cost
# 如果该状态未访问过,或找到了更优路径
if neighbor not in reached or new_g &lt; reached[neighbor]:
reached[neighbor] = new_g
child = Node(
state=neighbor,
parent=current,
action=f"{current.state}-&gt;{neighbor}",
g=new_g,
h=heuristic(neighbor)
)
heapq.heappush(frontier, child)
return None, float('inf') # 未找到路径
def romanian_map_problem():
"""罗马尼亚城市间的道路图(简化版)"""
图结构:城市 -> [(相邻城市, 距离), ...]
graph = {
'Arad': [('Zerind', 75), ('Sibiu', 140), ('Timisoara', 118)],
'Zerind': [('Arad', 75), ('Oradea', 71)],
'Oradea': [('Zerind', 71), ('Sibiu', 151)],
'Sibiu': [('Arad', 140), ('Oradea', 151), ('Fagaras', 99), ('Rimnicu Vilcea', 80)],
'Timisoara': [('Arad', 118), ('Lugoj', 111)],
'Lugoj': [('Timisoara', 111), ('Mehadia', 70)],
'Mehadia': [('Lugoj', 70), ('Drobeta', 75)],
'Drobeta': [('Mehadia', 75), ('Craiova', 120)],
'Craiova': [('Drobeta', 120), ('Rimnicu Vilcea', 146), ('Pitesti', 138)],
'Rimnicu Vilcea': [('Sibiu', 80), ('Craiova', 146), ('Pitesti', 97)],
'Fagaras': [('Sibiu', 99), ('Bucharest', 211)],
'Pitesti': [('Rimnicu Vilcea', 97), ('Craiova', 138), ('Bucharest', 101)],
'Bucharest': [('Fagaras', 211), ('Pitesti', 101), ('Giurgiu', 90), ('Urziceni', 85)],
'Giurgiu': [('Bucharest', 90)],
'Urziceni': [('Bucharest', 85), ('Hirsova', 98), ('Vaslui', 142)],
'Hirsova': [('Urziceni', 98), ('Eforie', 86)],
'Eforie': [('Hirsova', 86)],
'Vaslui': [('Urziceni', 142), ('Iasi', 92)],
'Iasi': [('Vaslui', 92), ('Neamt', 87)],
'Neamt': [('Iasi', 87)]
}
直线距离启发式(可采纳,永不高估真实距离)
straight_line_distance = {
'Arad': 366, 'Bucharest': 0, 'Craiova': 160, 'Drobeta': 242,
'Eforie': 161, 'Fagaras': 176, 'Giurgiu': 77, 'Hirsova': 151,
'Iasi': 226, 'Lugoj': 244, 'Mehadia': 241, 'Neamt': 234,
'Oradea': 380, 'Pitesti': 100, 'Rimnicu Vilcea': 193,
'Sibiu': 253, 'Timisoara': 329, 'Urziceni': 80, 'Vaslui': 199,
'Zerind': 374
}
def heuristic(state):
return straight_line_distance.get(state, 0)
return graph, heuristic
if name == "main":
运行示例:从 Arad 到 Bucharest
graph, heuristic = romanian_map_problem()
start = 'Arad'
goal = 'Bucharest'
print("=== A* 搜索罗马尼亚地图问题 ===")
print(f"起点: {start}, 终点: {goal}")
print()
path, total_cost = a_star_search(start, goal, graph, heuristic)
if path:
print("找到最优路径:")
print(" → ".join(path))
print(f"路径总代价: {total_cost}")
print()
print("关键步骤说明:")
print("1. 初始化: 将起点 Arad (g=0, h=366, f=366) 加入优先队列")
print("2. 扩展: 每次弹出 f 值最小的节点")
print("3. 启发式: 使用直线距离作为可采纳启发式 h(n)")
print("4. 剪枝: 通过 reached 字典记录每个状态的最佳 g 值")
print("5. 终止: 当弹出目标节点 Bucharest 时,重构路径并返回")
else:
print("未找到路径")
输出示例结果
print()
print("示例输出:")
print("=== A* 搜索罗马尼亚地图问题 ===")
print("起点: Arad, 终点: Bucharest")
print()
print("找到最优路径:")
print("Arad → Sibiu → Rimnicu Vilcea → Pitesti → Bucharest")
print("路径总代价: 418")
print()
print("(注: 实际运行结果可能因实现细节略有不同,但总代价应为 418)")</code></pre>
启发式设计:
- 松弛问题(relaxed problem)给出可采纳启发式(如八数码用"曼哈顿距离"或"错位棋子数")。
- 模式数据库(pattern database)预计算子问题代价。
- 从经验/学习获得启发式。
4. 关键图示/表格说明
搜索策略特性总表(见上 3 节表格),重点记忆:BFS 与 UCS 最优但耗空间;DFS 省空间但不优;A* 在可采纳/一致启发下最优。
A* 的 f、g、h 关系图(文字):随搜索推进,前沿上 f 值近似呈"等高线"扩张;一致 h 时每个状态只被最优扩展一次,最优路径沿 f 最小方向生长。
启发式强度示例(八数码):错位棋子数 h1 ≤ 曼哈顿距离 h2 ≤ 真实代价,二者皆可采纳,h2 信息更丰富、扩展节点更少(更" informed")。
5. 与其他章节的关联
- 状态空间与"问题形式化"是第 2 章 PEAS 在已知离散环境下的落地。
- 局部搜索、连续空间、非确定性环境是第 4 章对本章局限的扩展。
- 极小化极大与博弈树(第 5 章)可视为带对手的对抗搜索特例。
- CSP(第 6 章)用"变量+约束"重述组合搜索,避免显式枚举状态。
- 规划(第 11 章)与启发式学习(第 3 部分)复用 A* 思想。
6. 延伸思考
- A* 与大模型规划:LLM 做任务规划时常"一步到位"但易出错;能否把 A* 的可采纳启发式思想注入规划器,用模型给出的"完成度估计"作为 h(n),实现可验证的逐步最优规划?
- 搜索爆炸与近似:现实问题状态空间远超内存,A* 最优性代价过高。在自动驾驶等实时场景中,应如何权衡"最优解"与"足够好且即时"的解(转向启发式/局部搜索/采样)?