跳到内容

第 13 卷 / 第 1 章 / 第 1 课

1.1 状态空间、BFS、DFS 与一致代价搜索:先定义什么算同一个局面 ​

搜索算法的差别常被压缩成“队列还是栈”。真正决定正确性和成本的,是问题表示、边代价、重复状态和停止条件。

本课目标 ​

  • 用状态、动作、转移、目标和路径代价定义搜索问题;
  • 区分 tree search 与 graph search;
  • 说明 BFS、DFS 和 Uniform-Cost Search 的完整性与最优性条件;
  • 返回一条可验证路径,而不只是 True/False。

1. 一个搜索问题的六个部件 ​

可以把问题写成:

text
initial_state:从哪里开始
actions(state):当前允许哪些动作
transition(state, action):动作产生什么后继
goal_test(state):怎样算到达目标
step_cost(state, action, next_state):一步代价
state_key(state):怎样判断两个状态等价

路径是动作/状态序列;解是到达目标的路径;最优解还需要明确总成本。若目标只是“步数最少”,每步代价可以设为 1;若道路距离、风险或时间不同,就不能用 BFS 代替最低代价搜索。

2. 状态表示会改变搜索空间 ​

同一迷宫可表示为:

  • 像素、方向、速度和背包内容的完整世界状态;
  • 只保留当前位置的网格状态;
  • 把房间压成节点、走廊压成边的拓扑图。

抽象太细,分支和深度爆炸;抽象太粗,不同真实局面被合并,可能返回不可执行路径。

状态 key 必须包含所有会影响未来动作、目标和代价的信息。例如门是否打开会改变可达性,就不能只用坐标去重。相反,若到达某格的历史不影响以后,把整条路径放进 key 会阻止有效合并。

Tree search 把每条不同路径当成新节点。图中存在环时,它可能反复绕圈。Graph search 用 frontier 与 reached/visited 记录已发现状态,避免或控制重复。

但“见过就永不再看”只在特定算法下正确:

  • BFS 在单位边代价下,第一次发现状态已是最少步数;
  • DFS 第一次到达不保证最短或最低代价;
  • Uniform-Cost/A* 若发现更低 g,需要更新并可能重新打开状态。

如果 BFS 在弹出后才登记 visited,同一节点可能在弹出前多次入队。入队时登记可以避免这类重复;UCS 则记录当前最佳代价。队列里排了三份同一个状态,并不会让答案更有把握。

4. BFS:按深度逐层扩展 ​

下面是 Python 3 函数片段(partial)。调用者提供后继函数和目标判断;状态必须可哈希,且本例保留 None 作为父节点哨兵,不能用它表示真实状态。

python
from collections import deque

def reconstruct(parent, goal):
    path = [goal]
    while parent[path[-1]] is not None:
        path.append(parent[path[-1]])
    return list(reversed(path))

def bfs(neighbors, start, is_goal):
    frontier = deque([start])
    parent = {start: None}  # 同时表示 reached

    while frontier:
        state = frontier.popleft()
        if is_goal(state):
            return reconstruct(parent, state)

        for next_state in neighbors(state):
            if next_state not in parent:
                parent[next_state] = state
                frontier.append(next_state)

    return None

在分支因子有限、目标深度有限时,BFS 完整;每步代价相同且非负才保证最低代价。若最浅目标深度为 d、分支因子为 b>1,上面的实现在出队时判断目标,可能已经生成第 d+1 层,时间和空间上界为 O(bd+1)。改为生成时判断目标可得到常见的 O(bd) 界;两种写法都会很快吃满内存。

5. DFS:把内存换成搜索风险 ​

DFS 每次深入一个分支,适合:

  • 内存紧张;
  • 解可能很深;
  • 只需任意解;
  • 搜索树有限或有可靠深度界限。

它不保证最浅或最低代价解。在无限深分支中可能永远不到另一个近处目标。Graph DFS 使用全局 visited 时,空间还包括访问集合,不能笼统说“只需要 O(d)”;tree DFS 的 frontier 较小,但会重复状态。

可用 depth-limited search 防止无限下降,再逐步增加深度形成 iterative deepening DFS(IDDFS)。在单位代价、有限分支下,IDDFS 获得 BFS 的浅层最优性,同时保持接近 DFS 的 frontier 空间,代价是重复展开浅层。

6. Uniform-Cost Search:按累计代价扩展 ​

当边代价不同且非负时,使用优先队列,每次扩展当前累计代价 g 最小的状态。下面的 Python 3 片段复用上面的 reconstruct,edges(state) 返回 (后继状态, 有限非负代价)。

python
import heapq
from itertools import count

def uniform_cost_search(edges, start, is_goal):
    tie = count()
    frontier = [(0.0, next(tie), start)]
    best_g = {start: 0.0}
    parent = {start: None}

    while frontier:
        g, _, state = heapq.heappop(frontier)
        if g != best_g.get(state):       # 跳过旧队列项
            continue
        if is_goal(state):
            return reconstruct(parent, state), g

        for next_state, step_cost in edges(state):
            if step_cost < 0:
                raise ValueError("UCS requires non-negative edge costs")
            new_g = g + step_cost
            if new_g < best_g.get(next_state, float("inf")):
                best_g[next_state] = new_g
                parent[next_state] = state
                heapq.heappush(frontier, (new_g, next(tie), next_state))

    return None

UCS 与 Dijkstra 的核心一致。有限图允许零代价边;在无限图上,有限分支和每一步代价至少为某个正数 ε 是常用的完整性条件。仅限制正代价边还不够:无限长的零代价链也可能让搜索迟迟不取出目标。

7. 复杂度不只看节点数 ​

真实系统还要计算:

  • 生成后继的成本;
  • 状态 key 的大小与 hash 成本;
  • parent/动作存储;
  • 重复状态率;
  • 目标测试与边代价计算;
  • 图是否能预先物化。

隐式图(谜题、规划、博弈)通常不能列出全部节点,只能按需生成后继。

8. 正确性测试 ​

text
起点就是目标
目标不可达
有环和自环
同一状态由多条路径到达
单位代价与非单位代价
零代价边
多个目标
状态 key 过粗/过细

对小图可穷举所有简单路径或与可信最短路实现对照,验证路径连续、到达目标和总成本。

例如设 S→G 代价为 9,S→A 和 A→G 各为 2。按这个顺序提供后继时,BFS 返回 [S, G],UCS 返回 [S, A, G] 和总代价 4。再加入 A→S 的零代价边,两者仍应终止;这能同时检查去重和成本判断。

常见误区 ​

  • BFS 总能找到最短路:只在步数/等代价意义下。
  • DFS 空间一定是 O(d):graph search 还保存 visited。
  • 节点出队时再 visited 没区别:frontier 可能积累大量重复项。
  • 状态等于屏幕上看到的全部信息:应保留影响未来的最小充分状态。

练习 ​

  1. 为带钥匙和门的迷宫设计 state key,解释为何不能只用坐标。
  2. 在有环图上比较 tree BFS 与 graph BFS 的扩展次数。
  3. 构造 BFS 选少步高成本、UCS 选多步低成本的图。
  4. 为 UCS 增加 action 记录,并验证返回路径总成本。

小结 ​

搜索从问题建模开始。BFS、DFS 和 UCS 只是不同 frontier 策略;它们的保证依赖边代价、状态等价和搜索空间条件。

下一课加入启发式:A* 怎样使用目标下界减少无谓扩展,又为什么不一致启发式要求重新打开节点。

Built with VitePress | Software Systems Atlas