(d)深度优先搜索

官方信息技术老师·10 页·深入(追求细节与边界)·0 次浏览·3 天前
图遍历回溯与标记递归与栈

(d)深度优先搜索

看清递归调用栈、状态恢复、环检测的全部细节

按 空格/→ 演示下一步

1 / 10 页

全部页面点击任意一页,跳回舞台从这页播放

图遍历回溯与标记递归与栈

(d)深度优先搜索

看清递归调用栈、状态恢复、环检测的全部细节

1第 1 页 · (d)深度优先搜索

算法

上一页我们沿一条路走到底再回头——这就是 DFS。但「走到底再回头」这套动作为什么能被写成程序?它背后藏着所有算法共同的骨架。

定义
求解特定问题的有限步骤序列,是算法的本质
三大性质
有穷性、确定性、可行性,缺一不可
输入与输出
可有零或多输入,但至少要有一个输出
DFS 作为实例
递归终止=有穷、规则明确=确定、每步可执行=可行
厨房菜谱对应 →算法

菜谱规定步骤顺序与终点,对应有限性;照做必出菜,对应可行性

2第 2 页 · 算法

框架

DFS 的直觉是『一条路走到底、撞墙再换条』。要把它写成可复用的代码,就得先看清所有 DFS 题共用的那个框架。

回溯三步
先「做选择」进入分支,再递归处理子问题,最后「撤销选择」还原状态,准备尝试下一条
节点三色标记
未访问=0、路径上=1、已访问=2;三色才能正确处理环和重复遍历
终止与剪枝
撞到叶子、超出边界、或满足目标就返回;剪枝可提前砍掉无效分支
时间与空间
时间 O(节点+边),空间取决于递归深度,最坏情况退化为 O(节点数)
走迷宫选岔路对应 →DFS 回溯三步

选岔路=做选择,撞墙回头=撤销选择;标已走的门=三色标记,防止原地打转

3第 3 页 · 框架

细节

框架已经把 DFS 的骨架搭好了,但真正写代码、调错、对比方案时,几个细节没讲透会踩坑——这一页就把这些「里子」拆开。

本质动作:走到底再回头
不撞南墙不回头:递归到无路可走,才返回上一层换分支继续
DFS 序列不唯一
同一张图,邻接表的存储顺序不同,走出来的访问序列就不一样
必标 visited
无向图忘标记会死循环;递归深度过大还会爆栈
四类典型应用
连通块、环检测、拓扑排序、排列枚举——本质都是「走到底再回溯」
画家族族谱往上追溯对应 →DFS 的回溯机制

画到祖先无记录就擦回最近分叉,从旁支继续——一条路走到底

4第 4 页 · 细节

无向图

前面讲 DFS 时,默认图是有方向的——A 能到 B 不代表 B 能到 A。但现实中很多关系是双向的,比如好友、道路连接,这时就要换一种图来建模。

边无方向
边(u,v)与(v,u)是同一条,u能到v意味着v也能到u
邻接对称
邻接表中u的邻居里有v,v的邻居里也必有u
DFS更简单
遍历邻居时不用区分入边出边,逻辑只有一套
连通分量
一次DFS能访问到的所有顶点,构成一个连通块
微信好友关系对应 →无向图

你加我好友我也加你——关系天然对称,没有谁指向谁

5第 5 页 · 无向图

有向图

无向图里 A—B 和 B—A 是同一条边,但现实中很多关系是单向的——你关注了某人,他未必关注你。这一页看「有向图」。

边有方向
u→v 与 v→u 是不同的边,方向决定能否通行
入度与出度
入度=指向 v 的边数;出度=v 指出的边数
DFS 只沿箭头走
从 u 出发只能访问 u 的出边邻居,不能反向
强连通性
任意两顶点可互达;强连通分量是有向图独有结构
城市里的单行道系统对应 →有向图

路口是顶点,单行道是带箭头的边,车只能按箭头方向开

vdeg(v)=vdeg+(v)=E\sum_{v}\deg^{-}(v) = \sum_{v}\deg^{+}(v) = |E|
6第 6 页 · 有向图

多可达域

上一页我们把 DFS 的细节抠到了边和点的层面——递归如何推进、visited 何时置位。但有一个隐含前提:图是连通的。一旦图里出现互不相连的部分,这个前提就破了,单次 DFS 只能跑完其中一个区块。

可达域定义
从一个起点出发,DFS 能到达的所有顶点集合,即构成一个可达域
单次 DFS 边界
一次 DFS 只覆盖一个连通分量,碰到跨域边自然停——域与域之间无路径
全图遍历方式
对每个未访问顶点各启动一次 DFS,多棵 DFS 树组成 DFS 森林
域数等于启动次数
DFS 启动次数恰好等于图的连通分量个数,也等于可达域的个数
典型应用
连通分量计数、孤岛检测、网络模块划分、社交群组发现
海洋中的群岛对应 →图的多个可达域

每座岛是一个可达域,从一岛只能走遍该岛;要踏遍群岛必须从每座岛各派一支探险队

7第 7 页 · 多可达域

嵌套引理

上一页讲了DFS把图切成多个可达域。现在换个维度看'时间':每个顶点盖两个戳——发现时刻d和结束时刻f。这两数字围出一段区间,而所有区间之间的关系,整齐得令人惊讶。

时间区间
进入时打发现时间d,回溯时打完成时间f,构成 [d, f] 段
嵌套或分离
任意两顶点区间只可能完全包含或互不相交,绝不部分重叠
树的镜像
区间包含严格对应祖先-后代:祖先区间包住后代区间
边的判据
区间相对位置直接判定树边、后边、前边、横边四种类型
俄罗斯套娃对应 →DFS区间嵌套

进入新顶点像打开更小的套娃;返回时合上,区间一一对应

u 是 v 的祖先    d(u)<d(v)<f(v)<f(u)u\text{ 是 }v\text{ 的祖先}\iff d(u)<d(v)<f(v)<f(u)
8第 8 页 · 嵌套引理

本节要点

  • DFS 的主线是深入、回退、再转向
  • 判重让每次探索至多发生一次
  • 有向性改变可达语义,不改变遍历骨架
  • 跨域与嵌套问题都应守住访问不变量
延伸主题:强连通分量割边与割点拓扑排序
9第 9 页 · 本节要点

课后思考

先自己思考,再对照参考答案。三问分别对应:直觉回溯、实现细节、边界情况。

1为什么 DFS 用栈/递归而不是队列?这种选择如何体现了「先沿一条路走到底」的直觉?

参考答案DFS 要求「后进先出」——刚访问的节点要最先展开,所以选栈;队列是「先进先出」,会同时往所有方向铺开,那是 BFS 的行为。

2用显式栈模拟递归版 DFS,访问顺序是否完全一致?需要注意哪些细节?

参考答案理论上访问顺序一致,但有两个细节:入栈顺序要倒过来(如先压右孩子再压左孩子),且必须在入栈时就标记 visited,否则会重复入栈打乱顺序。

3在有环的有向图中,DFS 的「走到底」会遇到什么麻烦?该如何处理?

参考答案走到底再回退时会再次遇到环上节点,导致重复访问甚至死循环。解决方案是维护 visited 集合(或三色标记),访问过的节点直接跳过。

10第 10 页 · 课后思考