1. 概述

在本篇文章中,我们将探讨人工智能中两种常见的搜索策略:图搜索(Graph Search, GS)树状搜索(Tree-Like Search, TLS)。这两种策略用于解决AI代理从起点状态到目标状态的最优路径查找问题。

搜索算法的核心在于如何遍历状态图中的节点。GS 和 TLS 的主要区别在于是否记录已访问的状态,以避免重复探索甚至陷入死循环。


2. 搜索问题与状态图

搜索问题是指AI代理需要找到从起始状态到目标状态的最优动作序列。所有可能的状态及其之间的转换关系构成了一个状态图(State Graph)

一个状态可以是任意内容,取决于具体问题,比如:

  • 二维地图中的一个点
  • 产品组装顺序
  • 国际象棋棋子的布局

3. 搜索树

搜索算法的不同之处在于它们访问状态图节点的顺序。有些算法会构建一个搜索树(Search Tree),其根节点是起始状态。

3.1 搜索树与状态图的区别

  • 状态图中每个状态只出现一次
  • 搜索树中同一个状态可能多次出现,表示从起点到该状态的不同路径

3.2 前沿(Frontier)

在搜索过程中,我们区分“访问”和“扩展”两个概念:

  • 访问:找到从起点到某状态的路径
  • 扩展:遍历该状态的所有子状态

我们维护一个前沿集合(Frontier),记录已访问但未扩展的状态。

搜索策略包含两个核心规则:

✅ 是否将新节点加入前沿
✅ 从前沿中选择哪个节点进行扩展


4. 图搜索(Graph Search)

GS 的核心规则是:

不要将节点加入前沿,如果它的状态已经被扩展过,或者已经在前沿中存在

4.1 伪代码

algorithm GenericGraphSearch(start_state, goal_test, state_graph, choose_one, phi):
    frontier <- initialize the frontier to contain only the start node
    reached <- make a lookup table that will contain states
    
    while frontier is not empty:
        u <- choose_one(frontier)
        
        if u passes goal_test:
            return u
        
        for v in expand(u):
            if (v.state not in reached) and phi(v):
                Insert v.state into reached
                Add v to frontier
    
    return failure

4.2 实现细节

  • 节点对象应包含:

    • 当前状态
    • 父节点指针(用于回溯路径)
    • 动作(从父节点到当前节点)
    • 路径总成本
  • frontier 的实现依赖于具体算法:

    • DFS:使用 LIFO 队列(栈)
    • BFS:使用 FIFO 队列
    • UCS:使用优先队列(最小堆)
  • reached 可以用 Set 或 Map 实现,需支持快速查找和插入

4.3 搜索树的隐式表示

在图搜索中,搜索树是隐式的:

搜索树由 reached 中但不在 frontier 中的节点构成


5. 树状搜索(Tree-Like Search)

TLS 是图搜索的简化版本,不维护 reached 集合,即不记录已访问状态。

5.1 伪代码

algorithm GenericTreeLikeSearch():
    frontier <- initialize the frontier

    while frontier is not empty:
        u <- choose-one(frontier)

        if u passes goal_test:
            return u

        for v in expand(u):
            if phi(v):
                add v to frontier

    return failure

5.2 特点与问题

  • 不检查重复状态
  • 可能重复扩展同一状态
  • 可能导致无限循环

⚠️ 在含有环路的状态图中,TLS 容易陷入死循环


6. 示例:DFS 的两种实现

我们以一个简单图为例,比较 GS 和 TLS 在深度优先搜索(DFS)中的行为差异。

6.1 测试图结构

toy graph

其中 A 是起始状态,没有目标状态。

6.2 TLS DFS 实现

algorithm TLSDFS(start, goalTest, state_graph):
    s <- make the start node that contains the start state

    if goal_test(s):
        return s

    frontier <- initialize a LIFO queue containing only s

    while frontier is not empty:
        u <- pop the node most recently added to frontier

        for v in expand(u):
            if goalTest(v):
                return v
            Add v to frontier

    return failure

TLS DFS 的搜索路径如下图所示:

tls dfs

➡️ 结果:进入死循环,无法终止

6.3 GS DFS 实现

algorithm GSDFS(start_state, goal_test, state_graph):
    s <- make the start node that contains the start state

    if goal_test(s):
        return s

    frontier <- initialize a LIFO queue containing only s.state
    reached <- make a set {s}

    while frontier is not empty:
        u <- pop the node most recently added to frontier

        for v in expand(u):
            if goal_test(v):
                return v

            if v.state not in reached:
                add v to frontier

    return failure

GS DFS 的搜索路径如下图所示:

gs dfs

结果:成功避免死循环


7. 总结与讨论

特性 图搜索(GS) 树状搜索(TLS)
是否记录已访问状态 ✅ 是 ❌ 否
是否可能进入死循环 ❌ 否 ✅ 是
内存消耗 ⬆ 较高 ⬇ 较低
适用场景 含环图、需避免重复 无环图、内存受限

7.1 折中方案:防环搜索(Loop-Resistant Search)

可以仅检测路径是否构成环路,而非所有重复路径。这样可以在不记录所有状态的前提下避免死循环。

例如,只需判断当前节点是否在其父路径中出现过即可。

if v.state not in path_of(u):
    add v to frontier

8. 结论

  • 图搜索 更安全,避免重复和死循环,但内存开销大
  • 树状搜索 更轻量,但在含环图中可能陷入死循环
  • 折中方案 可在性能与安全性之间取得平衡

在实际应用中,应根据状态图的特性(是否有环、是否稀疏)选择合适的搜索策略。

踩坑提醒:在使用 TLS 时务必考虑图中是否存在环路,否则可能导致程序卡死或无限循环,特别是在图结构复杂或动态生成的场景中。


原始标题:Graph Search vs. Tree-Like Search