🌊

图的 BFS:水波纹,一圈一圈向外扩

DFS 是「一条路走到黑」,BFS 是水波纹:从起点出发,先通知直接相连的节点(第 1 圈),再由它们通知下一圈,一圈一圈、不越层。工具是队列(排队:先进先出)。水波一层层扩,所以节点第一次被到达,一定是最短路径(无权图:层数 = 最短步数)。

💡 打个比方

BFS = 往池塘扔石子,看波纹怎么扩散:石子落点(起点)先激起第 1 圈波纹——直接挨着起点的节点(第 1 层);这些节点又把波纹传给第 2 圈——挨着第 1 层的新节点(第 2 层);一圈一圈向外,永远先扩完这一圈,才轮到下一圈,绝不越层。记录「当前波纹前沿」的工具是队列——排队打饭:先进先出,先来先服务。算法:起点入队 → 每次取出队头一个节点,把它所有没访问过的邻居入队(它们就是下一圈)→ 直到队空。对比 DFS:DFS「一条路走到黑」靠递归/栈回头,BFS「一层层推进」靠队列排队。因为波纹按层扩,一个节点被第一次到达时,走的路一定最短(无权图里层数 = dist = 最短步数)——这是 BFS 最值钱的性质。

术语:BFS(breadth-first search,广度优先搜索:「一层层扩」)、队列(queue,先进先出 FIFO:front 队头、push 入队、pop 出队)、层(与起点的距离)、dist 数组(最短步数 = 层数)、无权图(每条边算 1 步)、多源 BFS(多个起点同时入队,一圈圈一起扩)。

🖼️ 大图看明白

BFS:水波纹一圈圈向外扩(层数 = dist = 最短步数) 第 0 层(起点) 第 1 层 第 2 层 第 3 层 第 4 层 1 2 3 4 5 6 0 1 1 2 3 4 队列推进:[1] → [2,3] → [3,4] → [4] → [5] → [6] → 空(dist:1→0,2、3→1,4→2,5→3,6→4)

🧩 子任务 1:BFS 思想——队列 + 层序扩散

🎯 小目标:说清 BFS 和 DFS 的分工(一层层 vs 一条路),会背 BFS 的「取队头 → 邻居入队」骨架,并知道「入队即标记」。
// DFS:一条路走到黑,靠递归(栈)回头
// BFS:一层层推进,靠队列排队(先进先出,像排队打饭:先来先服务)
//
// BFS 骨架(无权无向图,邻接表):
//   1. 起点 s 入队,vis[s] = true(入队即标记)
//   2. while 队列非空:
//        u = 取队头(并出队)
//        对 u 的每个邻居 v:若没访问过 → vis[v] = true,v 入队
//   3. 队空 = 所有能走到的节点都访问完了
//
// 关键细节:vis 在「入队那一刻」标,不是出队时才标
//   否则同一个 v 可能从两个邻居分别入队两次(重复排队,白干)
✅ 小检查:① 队列的核心性质是什么?② vis 应该在入队时标还是出队时标?为什么?③ DFS 的「回头」靠什么数据结构实现?
看看答案 ① 先进先出(FIFO):先入队的先处理——保证「当前圈」全部处理完才轮到「下一圈」。 ② 入队时标。若出队时才标:节点 v 同时是 u1、u2 的邻居,u1 处理时把 v 入队(还没标),u2 处理时又看到 v「没访问」→ 再入队一次 → v 排队两次,重复劳动。 ③ 栈(递归的调用栈):后进先出,正好是「走到黑、原路回头」。
⚠️ 易错:把 DFS 和 BFS 混用——「求最短步数」必须 BFS;「枚举所有走法/拓扑序」常用 DFS。另外队列操作记牢:push 入队(队尾)、front 看队头、pop 出队。

🧩 子任务 2:手推 BFS 分层遍历(6 节点小图)

🎯 小目标:对 6 节点图(边 1-2、1-3、2-4、3-4、4-5、5-6)从 1 出发,手推队列的每一步状态和每一层的节点,得出 dist。
// 图:边 1-2、1-3、2-4、3-4、4-5、5-6;起点 s = 1
//
// 队列状态推进(每次:取队头 → 它的未访问邻居入队并标 vis):
// 初始:q = [1],vis[1] = 1,dist[1] = 0
// 取 1:邻居 2、3 未访问 → 入队并标,dist = 1      q = [2, 3]
// 取 2:邻居 1(已访问)、4(未访问 → 入队,dist = 2) q = [3, 4]
// 取 3:邻居 1(已访问)、4(已访问,入队时就标了)→ 无新人 q = [4]
// 取 4:邻居 2、3(已访问)、5(未访问 → 入队,dist = 3) q = [5]
// 取 5:邻居 4(已访问)、6(未访问 → 入队,dist = 4) q = [6]
// 取 6:邻居 5(已访问)→ 无新人 q = [] 队空,结束!
//
// 分层结果:第 0 层 {1};第 1 层 {2, 3};第 2 层 {4};第 3 层 {5};第 4 层 {6}
// dist:1→0,2→1,3→1,4→2,5→3,6→4
✅ 小检查:① 节点 4 在第几层?dist[4] = ?② 「取 3」那一步,为什么 4 没有再次入队?③ 从起点到节点 6 的最短路径有几条、各几条边?
看看答案 ① 第 2 层,dist[4] = 2。 ② 4 在「取 2」那步入队时就被标记了;处理 3 时 vis[4] = 1 → 跳过。这正是「入队即标记」防重复。 ③ 2 条最短路径、各 4 条边:1-2-4-5-6 和 1-3-4-5-6(都等于 dist[6] = 4)。
⚠️ 易错:dist 是在入队时定的(dist[v] = dist[u] + 1,u 是「发现 v 的那一圈」),之后不会改——因为第一次发现一定最短。别想着「回头再优化」。

🧩 子任务 3:无权图最短路——为什么「第一层到达」一定最短?

🎯 小目标:会用「层不越界」论证 BFS 最短性;能举出 DFS 第一到达不最短的反例。
// 为什么 BFS 第一次到达 = 最短?(层不越界原则)
//   第 k 层的全部节点,一定在第 k+1 层任何节点入队之前处理完
//   (队列排队:先进先出,当前圈排完才轮到下一圈)
//   → 节点 v 若在第 k 层被发现,就不可能还有更短的路(否则它该在第 < k 层被发现)
//
// 反例:DFS 的第一到达可能不是最短
//   图:边 1-2、2-3、3-4、1-4(1 直接连着 4!)
//   DFS(1):1 → 2 → 3 → 4(顺序 1 2 3 4)——第一次到 4 走了 3 条边
//   但最短是 1 → 4 直接 1 条边!DFS「一头扎到底」错过了捷径
//   BFS(1):第 1 层 {2, 4}——4 直接被发现,dist[4] = 1 ✓
//
// 前提:无权图(每条边都算 1 步)。边有权重(高速 1 步 / 山路 10 步)→ BFS 不管用
✅ 小检查:① 上图(1-2、2-3、3-4、1-4)里 dist[4] 和 dist[3] 各是多少?② 为什么「第 k 层处理完才轮到第 k+1 层」?③ 边有权重时 BFS 还保证最短吗?该用什么?
看看答案 ① dist[4] = 1(直接边 1-4);dist[3] = 2(1-2-3 或 1-4-3 都是 2 步)。 ② 队列先进先出:第 k 层的节点先入队、先出队,它们的孩子(第 k+1 层)才入队——所以第 k 层一定先被处理完。 ③ 不保证(可能「绕远路但步数少」被先发现)。加权最短路用 Dijkstra 算法(用优先队列保证「代价最小的先处理」,进阶)。
⚠️ 易错:「BFS 求最短路」只适用于无权图(或所有边权相同)。看到「每条边长度/费用不同」就想 Dijkstra,别硬套 BFS。

🧩 子任务 4:多源 BFS(加餐)——多个起点同时入队

🎯 小目标:会做「多个起点同时扩散」的 BFS:所有起点 dist = 0 一起入队,跑一遍普通 BFS;理解结果 = 「到最近起点的距离」。
// 6 节点图(边 1-2、1-3、2-4、3-4、4-5、5-6),起点集合 {1, 6}(两个「水源地」)
//
// 初始化:dist[1] = 0、dist[6] = 0,q = [1, 6](一起入队!)
// 取 1:邻居 2、3 → 入队,dist = 1        q = [6, 2, 3]
// 取 6:邻居 5 → 入队,dist = 1           q = [2, 3, 5]
// 取 2:邻居 4 → 入队,dist = 2           q = [3, 5, 4]
// 取 3:邻居 4(已标记)→ 无新人          q = [5, 4]
// 取 5:邻居 4(已标记)、6(已访问)→ 无 q = [4]
// 取 4:邻居 2、3、5(全已访问)→ 队空,结束
//
// dist:1→0,6→0,2→1,3→1,5→1,4→2
// 含义:每个节点「离最近水源」的步数
//   应用:最近的消防栓 / 最近的充电桩 / 多个基地同时派援兵
//   (两个波纹同时扩,先碰到某点的波 = 最近的水源)
✅ 小检查:① 多源 {1,6} 时 dist[4] 是多少?分别离 1、离 6 几步?② 为什么多源 BFS 的结果 = 「到最近起点的距离」?③ 举一个现实应用。
看看答案 ① dist[4] = 2——离 1 是 2 步(1-2-4 或 1-3-4),离 6 也是 2 步(6-5-4),取最小还是 2。 ② 每个源的波纹同时一层层扩,同一时刻各源都处在「自己的第 k 层」——谁先到某点,谁的路一定 ≤ 别人,所以第一次到达 = 到最近源的最短路(原理和单源一样:层不越界)。 ③ 找最近的医院/消防栓/充电桩;迷宫里多个人同时出发谁先碰到目标;地图里「离任意主干道最近」的标注。
⚠️ 易错:多源 BFS 的初始化是所有源 dist = 0 全部入队(不是一个跑完再跑另一个);逐个跑再取 min 也能对,但要跑 k 次,慢得多。

💻 完整代码:起点到所有节点的最短距离(BFS + dist 数组)

// 题目:读入 n 节点 m 边的无权无向图和起点 s,输出 s 到每个节点 1..n 的最短距离(边数),不可达输出 -1。
// 输入:第 1 行 n m;接下来 m 行每行 u v(无向边);最后 1 行 s。
#include <iostream>
#include <queue>
#include <vector>
using namespace std;

const int N = 100005;
vector<int> adj[N];
int dist[N];           // dist[i] = s 到 i 的最短步数;-1 = 还没到达(兼当 vis)
int n, m;

int main() {
    cin >> n >> m;
    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    int s;
    cin >> s;
    for (int i = 1; i <= n; i++) dist[i] = -1;   // -1 当 vis 用:没到达过
    queue<int> q;
    dist[s] = 0;
    q.push(s);
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int i = 0; i < (int)adj[u].size(); i++) {
            int v = adj[u][i];
            if (dist[v] == -1) {        // 第一次到达 = 最短,立即标记
                dist[v] = dist[u] + 1;
                q.push(v);
            }
        }
    }
    for (int i = 1; i <= n; i++) {
        cout << dist[i];
        if (i < n) cout << " ";
    }
    cout << endl;
    return 0;
}

// 样例:n=6, m=6;边:1 2 / 1 3 / 2 4 / 3 4 / 4 5 / 5 6;s = 1
// 输出:0 1 1 2 3 4
//(dist = BFS 层数:1→0,2→1,3→1,4→2,5→3,6→4)

✋ 你来试试

第 1 题:6 节点图从 1 做 BFS:dist[6] = ?1 到 6 的最短路有几条、各几条边? dist[6] = 4;最短路径 2 条、各 4 条边:1-2-4-5-6 和 1-3-4-5-6。
第 2 题:边 1-2、2-3、3-4、1-4 的图,从 1 做 BFS:每个节点的 dist? dist:1→0,2→1,4→1(直接边 1-4!),3→2(从 2 或 4 发现,都是 2 步)。
第 3 题:6 节点图做多源 BFS(起点 {1, 6}):每个节点的 dist? 1→0,6→0,2→1,3→1,5→1,4→2(离 1、离 6 都是 2 步,取最小 = 2)。
第 4 题(加餐):BFS 的时间复杂度是多少?(提示:每个节点入队几次?所有邻接表加起来被扫了多少遍?) O(n + m):每个节点恰好入队/出队一次(O(n));每个节点出队时扫一遍它的邻接表,所有表加起来共 2m 项(O(m))。空间 O(n)(队列 + dist)。
🎯 一句话记住:BFS = 水波纹——队列装「当前波纹前沿」,每轮向外扩一层;入队即标记,每个节点恰好入队一次;层数 = dist = 无权图最短步数(第一次到达一定最短,因为层不越界);多个起点 → 全部一起入队 = 多源 BFS(到最近起点的距离)。