Breadth First Search
广度优先搜索
广度优先搜索(Breadth-First Search, BFS)是图的遍历算法。它从某个起始顶点出发,先访问距离起点最近的一层顶点,再访问下一层顶点。实现上的核心工具是队列:先被发现的顶点先扩展。
从树的层序遍历到图的 BFS
树的层序遍历可以直接用队列:
- 根结点入队。
- 队头结点出队并访问。
- 将该结点的孩子依次入队。
- 重复直到队列为空。
图的 BFS 与它非常像,但图可能有回路。搜索某顶点的邻接点时,可能遇到已经访问过的顶点,所以 BFS 必须额外维护 visited[] 数组。
对一个新邻接点,必须在它入队时就标记 visited[w] = true。否则同一个顶点可能在真正出队前,被多个已访问顶点重复发现并重复入队。
| 问题 | 典型接口或结构 | 说明 |
|---|---|---|
| 怎样找到某顶点的邻接点 | FirstNeighbor(G, v) 与 NextNeighbor(G, v, w) |
不直接依赖具体存储结构,邻接矩阵和邻接表都可以实现这两个接口 |
| 怎样避免重复访问 | visited[] |
顶点第一次被发现时置为 true |
| 怎样按层扩展 | 队列 Queue |
队头顶点出队并扩展,它发现的新顶点进入队尾 |
FirstNeighbor(G, v) 返回顶点 v 的第一个邻接点;若不存在,返回 -1。NextNeighbor(G, v, w) 在已知 w 是 v 的某个邻接点时,返回 v 在 w 之后的下一个邻接点;若 w 已是最后一个邻接点,返回 -1。
队列推进过程
下面的示例从顶点 2 出发。若邻接表中各链表的顺序固定为:
1 | 1: 2, 5 |
则 BFS 序列为:
1 | 2, 1, 6, 5, 3, 7, 4, 8 |
BFS 的“按层”不是靠递归深度,而是靠队列顺序自然形成:
- 起点先入队,是第 0 层。
- 起点的所有未访问邻接点依次入队,是第 1 层。
- 第 1 层顶点依次出队时,它们新发现的顶点进入队尾,是第 2 层。
- 以此类推。
C 代码
下面用 FirstNeighbor 和 NextNeighbor 屏蔽存储结构差异。考试写伪代码时通常只要把这两个接口说明清楚即可;写 C 时可以把队列看成已经实现好的顺序队列或链队列。
1 | // Visits all vertices reachable from `start` by breadth-first search. |
非连通图版
单次 BFS(G, start) 只能访问从 start 可达的那些顶点。若图是非连通无向图,只从一个顶点出发就无法遍历完整个图。
非连通图版写法是在外层扫描所有顶点:遇到尚未访问的顶点,就以它为起点再调用一次 BFS。
1 | // Traverses the whole graph by BFS. |
对无向图,有一个非常常考的结论:
对无向图执行完整 BFS 遍历时,BFS 函数的调用次数等于图的连通分量数。
若无向图连通,只需调用一次;若有 $k$ 个连通分量,就需要调用 $k$ 次。
设有向图的弧为:1->5,2->1,3->6,4->3,4->7,6->2,7->3,7->6,7->8,8->4。
若完整遍历先从 1 调用 BFS,只能访问 1, 5;之后还要从 2、3、4 这些尚未访问顶点继续启动 BFS,才能覆盖全图。
若从 7 调用 BFS,则可沿 7->3->6->2->1->5 和 7->8->4 覆盖所有顶点,因此只需调用一次。
所以有向图的调用次数不能只看“弱连通外形”,要看从起点沿出边实际能到达哪些顶点。
BFS 遍历序列
同一个图,从不同起点出发,BFS 遍历序列通常不同。例如在上面的无向图中:
- 从顶点
1出发,可得到1, 2, 5, 6, 3, 7, 4, 8。 - 从顶点
2出发,可得到2, 1, 6, 5, 3, 7, 4, 8。 - 从顶点
3出发,可得到3, 4, 6, 7, 8, 2, 1, 5。
即使起点相同,BFS 序列也可能受存储结构影响:
若顶点 6 的邻接表写作 2, 3, 7,从 2 出发时会先发现 3 再发现 7。
若顶点 6 的邻接表写作 2, 7, 3,则 7 会先于 3 入队,后续 BFS 序列和生成树都可能改变。
在 BFS 过程中,一个顶点第一次被发现时,若是通过边 $(u,v)$ 从 u 发现 v,就把这条边记录为生成树边。所有这样的发现边构成广度优先生成树。
广度优先生成树的特点:
- 树根是 BFS 的起始顶点。
- 树中第 $i$ 层顶点对应图中距离起点为 $i$ 的顶点。
- 只有“第一次发现新顶点”的边进入生成树。
- 图中其余边仍存在,但不是生成树边。
由于广度优先生成树由 BFS 过程确定,所以它也受邻接点访问顺序影响:
- 基于邻接矩阵时,若顶点编号和扫描规则固定,BFS 序列与 BFS 生成树通常唯一。
- 基于邻接表时,邻接表顺序不唯一,BFS 序列与 BFS 生成树也可能不唯一。
对非连通图执行完整 BFS 遍历时,每个连通分量会得到一棵 BFS 生成树,所有这些树合起来称为广度优先生成森林。
复杂度
BFS 的辅助空间主要来自队列和 visited[]:
$$
O(\lvert V\rvert)
$$
时间复杂度取决于存储结构:
| 存储结构 | 顶点访问 | 邻接点查找 | 总时间复杂度 |
|---|---|---|---|
| 邻接矩阵 | $O(\lvert V\rvert)$ | 每个顶点都要扫描一整行,合计 $O(\lvert V\rvert^2)$ | $O(\lvert V\rvert^2)$ |
| 邻接表 | $O(\lvert V\rvert)$ | 每条边或弧至多被检查有限次,合计 $O(\lvert E\rvert)$ | $O(\lvert V\rvert+\lvert E\rvert)$ |
对非连通图,完整遍历会多次调用 BFS。但所有调用加起来,每个顶点只会被访问一次;用邻接表时,所有边或弧也只会被扫描有限次,因此总复杂度仍是 $O(\lvert V\rvert+\lvert E\rvert)$。
- BFS 使用队列,DFS 使用递归或栈。
- BFS 适合求无权图从起点到其他顶点的最短路径,这一点后续最短路径部分会继续使用。
- 访问新顶点时立即标记
visited,再入队。 - 单次 BFS 只能覆盖从起点可达的部分。
- 无向图完整 BFS 的调用次数等于连通分量数。
- BFS 生成树记录的是“第一次发现顶点”的边。