云计算百科
云计算领域专业知识百科平台

从基础理论开始学习人工智能(四):盲目搜索——深度优先、广度优先与迭代加深

从基础理论开始学习人工智能(四):盲目搜索——深度优先、广度优先与迭代加深

系列:AI 学习笔记 · 从零开始读教材
教材:《人工智能》(第 3 版)第 2 章「盲目搜索」2.3 节(深度优先搜索 DFS / 广度优先搜索 BFS / 迭代加深的深度优先搜索 DFS-ID)


一、引言:从"怎么走"到"按什么顺序走"

第 03 篇我们完成了 2.0~2.2.3 的学习:把问题表示成状态空间图,再沿着生成-测试范式改进出回溯法与贪心算法。但有一个关键问题还没有系统回答——

在状态空间图上,到底按什么顺序去扩展节点,才能高效地找到解?

教材 2.3 节正式回答这个问题。它给出三种最经典的**盲目搜索(blind / brute-force search)**算法:深度优先搜索(Depth-First Search, DFS)、广度优先搜索(Breadth-First Search, BFS)和迭代加深的深度优先搜索(Depth-First Iterative Deepening, DFS-ID)。

“盲目"的含义是:搜索过程不使用任何关于问题域的知识(不像启发式搜索那样"猜"哪个方向更可能接近目标),只是机械地按某种既定顺序把状态空间图"走"一遍。虽然"盲目”,但它们是理解一切更高级搜索算法的地基,也是本教材第 4 章启发式搜索的对照基线。


二、2.3 总览:三个算法、两种数据结构、一个思想

把 2.3 节浓缩成一张速查表,先建立整体印象:

算法数据结构扩展顺序一句话
深度优先搜索 DFS 栈(Stack,LIFO) 沿着一条分支尽可能深入 能深则深,无路可走就回溯
广度优先搜索 BFS 队列(Queue,FIFO) 按距根的层数逐层铺开 层层推进,先到先得
迭代加深 DFS-ID 栈 + 深度界限 每轮限制深度地做 DFS,界限逐轮加 1 用重复换内存,兼得两者

三种算法都建立在 open 表 / closed 表 的框架上:

  • open 表:存放已生成但尚未扩展的节点(“待办清单”);
  • closed 表:存放已经扩展过的节点(“已完成清单”),避免重复访问。

算法的全部差别,只在于"从 open 表中取出下一个节点的顺序":DFS 从栈顶取(后进先出),BFS 从队首取(先进先出),DFS-ID 则反复用 DFS 但每次限定最大深度。

在这里插入图片描述

图 19 深度优先搜索的过程演示:从根节点出发,沿最左侧分支一路深入(1→2→4→8),直到叶子节点无后继;此时回溯到上一层,再深入下一个未访问分支(9、5…)。图中箭头表示扩展顺序,虚线表示"碰壁后的回溯"。


三、2.3.1 深度优先搜索(DFS)

3.1 核心思想:一条路走到黑,撞墙再回头

深度优先搜索的基本策略是:

在搜索树的任意一层,都只取当前节点的一个子节点继续深入;当到达没有后继节点(叶子)或没有未访问后继的节点时,就**回溯(backtrack)**到上一层的下一个未访问节点,继续重复这个过程。

换句话说,DFS 的信念是:“只要沿着一条分支不断深入,总有一刻会碰到目标;这条路走不通,就退回去换一条。” 它优先探索"深度",而不是"广度"。

3.2 数据结构:栈(LIFO)与 open/closed 表

DFS 对应数据结构是栈(stack):后进先出(LIFO, Last-In-First-Out)。

搜索开始时,把起始节点压入 open 栈;每次循环:

  • 弹出 open 栈顶节点 x;
  • 检查 x 是否目标状态——是则成功返回;
  • 否则把 x 的所有子节点压入 open 栈顶(后生成的子节点下次先被弹出),并把 x 移入 closed 表;
  • 重复,直到 open 栈为空(无解)或找到目标。
  • "栈"决定了 DFS 天然的行为:新发现的节点永远优先被扩展,于是搜索总是扎进当前分支的最深处;只有整条分支都弹空(回溯),才轮到上一层兄弟节点。

    3.3 过程演示:一棵 3 层二叉树的 DFS 顺序

    在这里插入图片描述

    图 20 广度优先搜索的过程演示:先把起始节点的全部子节点加入队列,再按"先入队先扩展"的顺序逐层铺开(1→2→3→4→5→…)。扩展顺序严格按节点距根的层数:第 1 层全部访问完,才进入第 2 层。

    回到 DFS,它的访问顺序可以用下面这棵小树直观感受(这也是图 19 的底层结构):

    • 根节点记为 1,其子节点 2、3;2 的子节点 4、5;4 的子节点 8、9……
    • DFS 的访问序列是:1 → 2 → 4 → 8 → 9 → 5 → 3 → 6 → 7(先沿 1-2-4-8 深入到底,8 无子节点后回溯到 4,再扩展 9;9 到底后回溯到 2,扩展 5……以此类推)。

    注意与 BFS 的对照:BFS 会先访问 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9(逐层铺开),而 DFS 则是"垂直到底再回来",两者的访问顺序截然不同。

    3.4 伪代码

    在这里插入图片描述

    图 21 DFS 伪代码:open 为栈(LIFO),closed 记录已扩展节点;每次从 open 弹出栈顶,生成子节点时压栈,并跳过已在 open/closed 中的节点(防止环路与重复)。

    教材风格的 DFS 伪代码如下:

    Procedure depth_first_search
    open = [Start] # 栈:初始只含起始节点
    closed = [] # 已扩展节点表
    while open ≠ [] do
    x = pop(open) # 取栈顶(最后压入的节点)
    if x 是目标状态 then
    return x # 找到解
    for x 的每个子节点 c do
    if c ∉ open 且 c ∉ closed then
    push(c, open) # 压入栈顶,优先扩展
    push(x, closed) # 记录已扩展
    return FAIL # open 为空:无解

    3.5 特性与代价

    特性DFS 的表现
    空间复杂度 O(b·m)——只需保存"当前路径 + 路径上节点的未访问兄弟",b 为分支因子、m 为最大深度
    时间复杂度 O(bᵐ)——最坏情况下要探索整棵深度为 m 的树
    完备性 不保证——若状态空间含无限深分支(或环),可能一头扎进去永不返回;实际实现靠 closed 表避免环,但无限分支仍需深度界限
    最优性 不保证——先找到的未必是最短路径

    适用场景:状态空间很深但分支不宽、解的位置"大概率靠深"时很划算;也是空间受限环境下(内存小)的首选。教材指出,DFS 在"无效路径不太长"的问题上表现得合理——因为这样即使走错路,回溯成本也不高。


    四、2.3.2 广度优先搜索(BFS)

    4.1 核心思想:层层推进,先到先得

    广度优先搜索与 DFS 恰好相反:

    按照节点离根节点的距离(层数)来生成节点——先访问距离为 1 的所有节点,再访问距离为 2 的所有节点,依此类推。第一个到达目标的路径,一定是长度最短的路径。

    BFS 的信念是:“先把眼前这一层看全,再往下一层走。” 它优先保证"离起点近的节点先被处理"。

    4.2 数据结构:队列(FIFO)与 open/closed 表

    BFS 对应数据结构是队列(queue):先进先出(FIFO, First-In-First-Out)。

    搜索过程与 DFS 几乎相同,唯一区别在取出顺序:

  • 出队 open 队首节点 x(最早进入的节点先被处理);
  • 检查 x 是否目标——是则成功返回;
  • 否则把 x 的所有子节点加入队尾,x 移入 closed 表;
  • 重复,直到队列为空或找到目标。
  • "队列"保证了同一层的节点一定先于下一层被扩展:父节点先入队,它的子节点排在所有已有兄弟之后,于是天然形成"逐层推进"的顺序。

    4.3 过程演示与最优性

    回到图 20 那棵 3 层二叉树:BFS 的访问序列是 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9,一层一层地推进。

    由于 BFS 永远在"更浅的层"里先找目标,所以:

    在每条边的代价相同(单位代价)的前提下,BFS 找到的第一个目标一定位于最浅深度 d,即最优解。

    这是 BFS 最宝贵的性质,也是 DFS 没有的。代价是空间:BFS 需要把当前层的所有节点都保存在内存里。

    4.4 伪代码

    在这里插入图片描述

    图 22 BFS 伪代码:open 为队列(FIFO);每次从队首取出节点,生成子节点时加入队尾,保证按层扩展;同样用 open/closed 表去重。

    Procedure breadth_first_search
    open = [Start] # 队列:初始只含起始节点
    closed = [] # 已扩展节点表
    while open ≠ [] do
    x = dequeue(open) # 取队首(最早进入的节点)
    if x 是目标状态 then
    return x # 找到解(且是最浅解)
    for x 的每个子节点 c do
    if c ∉ open 且 c ∉ closed then
    enqueue(c, open) # 加入队尾,等待下一轮
    enqueue(x, closed) # 记录已扩展
    return FAIL

    4.5 特性与代价

    特性BFS 的表现
    空间复杂度 O(bᵈ)——要保存当前层全部节点,d 为最浅解深度
    时间复杂度 O(bᵈ)——最坏要探索到第 d 层的全部节点
    完备性 完备(状态空间有限时)——只要解存在,逐层推进迟早找到
    最优性 最优(单位代价)——第一个找到的路径深度最小

    适用场景:需要最短路径、或者解大概率很浅的问题;状态空间宽度可控时 BFS 表现很好。但若树又宽又深,BFS 的内存开销会爆炸——这正是 DFS-ID 出场的原因。


    五、2.3.3 迭代加深的深度优先搜索(DFS-ID)

    5.1 痛点:DFS 省空间但不完备,BFS 完备但费空间

    把前面两节对照一下,会出现一个两难:

    • DFS:空间省(O(bm)),但可能不完备、不最优;
    • BFS:完备且最优(单位代价),但空间爆炸(O(bᵈ))。

    有没有可能"既像 DFS 一样省内存,又像 BFS 一样保证找到最浅解"?

    教材给出的答案是:迭代加深的深度优先搜索(Depth-First Iterative Deepening, DFS-ID)——它通过反复执行"带深度限制的 DFS",并逐轮把深度界限加 1,从而一举兼得两者。

    5.2 核心思想:逐轮放宽"深度界限"

    DFS-ID 的做法: 先执行深度界限为 0 的 DFS(只看根节点),若未找到解,再执行深度界限为 1 的 DFS(看到第 1 层),再执行深度界限为 2 的 DFS……每轮都从根节点重新开始,深度界限逐轮递增,直到找到解为止。

    注意一个关键点:每一轮都要从头重新搜索,之前轮次的结果不能直接复用。表面上看这是巨大的浪费,但事实上——

    • 树的分支结构意味着浅层节点只占极小比例:深度为 d 的完全 b 叉树,第 d 层有 bᵈ 个节点,而前 d-1 层总共只有 (bᵈ−1)/(b−1) ≈ bᵈ/(b−1) 个。也就是说,重复搜索浅层的额外开销只有大约 b/(b−1) 倍(常数倍);
    • 因此 DFS-ID 的总时间复杂度仍是 O(bᵈ),与 BFS 同阶,却只花 O(b·d) 的空间(像 DFS 一样省内存)。

    5.3 过程演示

    在这里插入图片描述

    图 23 DFS-ID 过程演示:第 1 轮只允许深度 0(仅根);第 2 轮允许深度 1(访问到第 1 层);第 3 轮允许深度 2(访问到第 2 层)……每轮都从根重新开始,直到某轮在界限内发现目标。右侧标出"每轮重做浅层"的代价其实只是常数倍。

    用一棵目标在第 3 层的小树感受过程:

    • 轮次 0(界限 d=0):只检查根节点,不是目标;
    • 轮次 1(d=1):从根重新出发,检查到第 1 层,不是目标;
    • 轮次 2(d=2):从根重新出发,检查到第 2 层,不是目标;
    • 轮次 3(d=3):从根重新出发,检查到第 3 层,找到目标,停止。

    因为每轮都按"先深后浅、但不超过界限"的顺序展开,而界限又逐轮放宽,所以第一次找到目标的轮次,深度界限恰好等于最浅解深度 d——这正是 BFS 的最优性。而任意时刻内存里只保留"当前深度界限内的一条路径",空间又回到 DFS 的量级。

    5.4 伪代码

    Procedure depth_first_iterative_deepening
    for d = 0, 1, 2, … do # 深度界限逐轮递增
    result = depth_limited_search(Start, d)
    if result ≠ cutoff then
    return result # 找到目标(或确认无解)

    Procedure depth_limited_search(x, limit)
    if x 是目标状态 then
    return x
    if depth(x) = limit then # 已达本轮界限
    return cutoff # 返回"该层截断"标记
    for x 的每个子节点 c do
    result = depth_limited_search(c, limit)
    if result ≠ cutoff then
    return result
    return cutoff # 此分支在界限内无解

    (实现细节:外层循环也可以从 d=1 开始;cutoff 是一个特殊标记,表示"本轮界限内没找到,但可能更深的地方有解",触发下一轮。)

    5.5 特性与代价

    特性DFS-ID 的表现
    空间复杂度 O(b·d)——与 DFS 同阶,远小于 BFS 的 O(bᵈ)
    时间复杂度 O(bᵈ)——与 BFS 同阶(常数因子 b/(b−1))
    完备性 完备(状态空间有限时)——界限逐轮递增,覆盖所有深度
    最优性 最优(单位代价)——第一轮命中的解深度最浅
    渐进最优性 树搜索中时间与空间渐进最优——在所有能找到最优解的盲目搜索中,没有任何算法同时在时间与空间上比它更好

    教材特别强调最后一点:

    深度优先的迭代加深(DFS-ID)在树搜索中,是"时间 + 空间"意义上渐进最优的盲目搜索算法——想要最优解,又想省内存,DFS-ID 就是盲目搜索的终极答案。

    另一个值得一提的性质:DFS-ID 是一种"任意时刻算法(anytime algorithm)"——它可以在任意时刻被中断,并返回当前已完成轮次所能给出的当前最优解。这在实时系统里非常实用。


    六、三种算法对比:一张表看懂

    在这里插入图片描述

    图 24 三种盲目搜索算法对比总表:从数据结构、扩展顺序、完备性、最优性、时间与空间复杂度、典型场景六个维度并排比较,底部给出记忆口诀。

    指标深度优先 DFS广度优先 BFS迭代加深 DFS-ID
    数据结构 栈(LIFO) 队列(FIFO) 栈 + 深度界限
    扩展顺序 沿分支深入,无路回溯 按层逐层铺开 每轮限制深度地 DFS,界限逐轮 +1
    完备性 不保证(可能陷入无限深分支) 完备(有限状态空间) 完备(有限状态空间)
    最优性 不保证 最优(单位代价) 最优(单位代价)
    时间复杂度 O(bᵐ) O(bᵈ) O(bᵈ)(常数倍 b/(b−1))
    空间复杂度 O(b·m) O(bᵈ) O(b·d)
    典型场景 空间受限、解偏深 求最短路径、解偏浅 深度未知、要最优又要省内存

    记忆口诀:解深用 DFS,解浅用 BFS,深度未知要最优省内存,就上 DFS-ID。(符号约定:b = 分支因子;d = 最浅解深度;m = 状态空间最大深度。)


    七、适用场景总结

  • DFS:内存紧张、问题解"藏在深处"、无效路径不长时是首选;但要用深度界限或 closed 表兜底,防止无限深入。
  • BFS:明确要最短路径、状态空间宽度可控时最稳;代价是内存随深度指数增长,树太宽会撑爆内存。
  • DFS-ID:不知道解有多深、又希望同时拿到"省内存 + 最优解"时,几乎总是最优选择;它牺牲少量常数倍时间,换来 BFS 的最优性与 DFS 的空间效率。
  • 三者都是盲目搜索:不使用问题域知识。若想在同样的框架下"猜"得更聪明——比如优先扩展"看起来最接近目标"的节点——就需要进入教材后续的启发式搜索(第 4 章),那里将给出贪心最佳优先、A* 等算法。


    八、总结:整篇思维导图

    把 2.3 节全部知识点收拢成一张导图,复习时一眼掌握:

    在这里插入图片描述

    图 25 第 04 篇总结导图。中心主题「2.3 盲目搜索」,六个主分支:DFS 深度优先、BFS 广度优先、DFS-ID 迭代加深、算法评价(完备性/最优性/时间/空间)、公共基础(状态空间图、open/closed 表、生成-测试范式)、选型建议。


    九、学习心得与下一步

    9.1 学习心得

  • 三种算法的差别,本质是"从 open 表里取节点的顺序"。把 2.3 节学完最大的收获是:DFS 和 BFS 的代码骨架几乎一模一样,唯一不同的就是"用栈还是用队列"。数据结构决定搜索行为——栈让你深钻,队列让你平推。理解这一点,比背任何实现都有用。
  • DFS-ID 是"用时间换空间"的教科书案例。初看"每轮都从头重搜"简直是浪费,但算一笔账就释然了:完全 b 叉树里,浅层节点数只占整棵树的 (b−1) 分之一,重搜浅层的额外开销只是常数倍。于是 DFS-ID 以 O(bᵈ) 的时间拿到了 BFS 的最优性,又以 O(b·d) 的空间保留了 DFS 的省内存——这正是"渐进最优"四个字的分量。
  • 完备性、最优性、时间、空间,是评价一切搜索算法的四把尺子。以后学 A*、IDA*、迭代加宽等算法,都可以用这套指标去"体检":它完备吗?最优吗?时间多少?空间多少?四问下来,算法画像就清晰了。
  • "盲目"是暂时的,但不可或缺。盲目搜索不利用问题域知识,看似笨拙,却是启发式搜索的起点与基线——没有 BFS 的最优性作参照,就无法量化启发式带来的加速;没有 DFS 的空间优势,就无法理解 IDA* 的设计动机。地基打得牢,上层才立得住。
  • 参考资料与说明

    • 教材内容与引文出处:《人工智能》(第 3 版)第 2 章「盲目搜索」2.3 节:2.3.1 深度优先搜索、2.3.2 广度优先搜索、2.3.3 迭代加深的深度优先搜索(DFS-ID)。复杂度记号 b/d/m 的含义、渐进最优性结论均按教材与经典 AI 教材(Russell & Norvig AIMA)对照整理。
    • 伪代码为教材风格的重述:DFS 使用栈 LIFO 与 open/closed 表;BFS 使用队列 FIFO 与 open/closed 表;DFS-ID 使用带深度界限的深度受限搜索,逐轮递增界限。
    • 复杂度说明:DFS 最坏时间 O(bᵐ)、空间 O(b·m);BFS 时间与空间均为 O(bᵈ);DFS-ID 时间 O(bᵈ)(常数因子 b/(b−1))、空间 O(b·d);"树搜索中时间与空间渐进最优"为教材明确结论。
    • 配图说明:本文所有示意图均为自绘矢量图(SVG 渲染为高清 PNG),全部中文标注,无任何 AI 生成水印/logo/角标;字号层级统一(标题 15–16px / 正文 12–13px / 注释 10–11px),已逐张视觉质检:无乱码、无重叠、无连线压字。
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 从基础理论开始学习人工智能(四):盲目搜索——深度优先、广度优先与迭代加深
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!