🌊

广度优先搜索 BFS

一圈一圈往外扩——波纹先碰到的岸,就是最近的岸

💡 打个比方

往池塘中央扔一颗石子,波纹一圈一圈往外扩。岸离石子 1 步的,第 1 圈就被波及;离 2 步的,第 2 圈才轮到。广度优先搜索(BFS,Breadth-First Search)就是"波纹算法":从起点出发,先把离它 1 步的所有点走完,再走 2 步的,再走 3 步的……。谁被波纹第一次碰到,那个圈数就是最短步数——因为波纹是一步一步扩的,不可能跳过更近的岸。

管理"下一圈要访问谁"的工具是队列(queue):排队买冰淇淋,先来的先服务(FIFO)。上一课 DFS 用"调用栈"走到底,这一课 BFS 用队列一层一层推——两种搜索,两种气质。

BFS:从起点开始,按"距离"一圈一圈访问:先访问所有 1 步邻居,再所有 2 步点……用队列(先进先出)维护待访问点,dist 数组记录每个点到起点的步数(-1 = 还没到)。无权图中,首次到达 = 最短步数。

🖼️ 大图看明白

还是上一课的演练场,这次看波纹:从 1 号站扔石子,节点颜色 = 波纹第几圈碰到它(dist 值)。紫 = 0 圈(起点),蓝 = 1 圈,橙 = 2 圈,绿 = 3 圈;7 号孤岛永远碰不到(dist = -1)。

BFS 波纹:从 1 出发,节点颜色 = dist(第几圈到达) 1 2 3 4 5 6 7 0 1 1 2 2 3 -1 图例:白底小圆 = dist 值;灰虚线大圆 = 孤岛(波纹永远到不了,dist 保持 -1) 扩圈顺序:第0圈 1 → 第1圈 2 3 → 第2圈 4 5 → 第3圈 6;最短步数 1→6 = 3(1-2-4-6 或 1-3-5-6)

🧩 子任务 1:为什么"先碰到"就是"最近"?

🎯 小目标:用波纹说理,证明 BFS 第一次到达 = 最短步数

波纹一圈只扩一步。假设某条路 3 步能到 6 号站,那波纹第 3 圈一定会碰到它——不可能第 3 圈还没到、第 4 圈才到(那说明 3 步的路走不通,矛盾)。所以首次被碰到的圈数,就是最短步数,一个不多一个不少。

对照演练场:6 号站在第 3 圈才被碰到(1→2→4→6 或 1→3→5→6,都是 3 步)。DFS 上一课找到的"1→2→4→6→5"是 4 条边的绕路——BFS 不会犯这个错,因为它先看所有 1 步、再看所有 2 步,近的永远先于远的。

前提:无权图。每条边都算"1 步"时 BFS 才保证最短路。如果边有权重(高速公路 5 分钟 vs 土路 20 分钟),"圈数"就不等于时间了——加权最短路是更高级的算法(Dijkstra),GESP6 主要考无权。
✅ 小检查:从 1 出发,第 2 圈(dist=2)能碰到哪些站?1 到 5 的最短步数是几?
看看答案
第 2 圈碰到 4 和 5(1→2→4、1→3→5,都是 2 步)。1→5 最短步数 = 2(直接 1-3-5)。

🧩 子任务 2:BFS 过程——队列排队 + dist 记账

🎯 小目标:能手推 BFS 的每一步(队列里是谁、dist 怎么变)

工具两样:队列(排队,先进先出)+ dist 数组(初始全是 -1 = 没到过,起点 dist = 0)。规则:出队一个 u → 看它所有邻居 → 没到过(dist=-1)的,dist 记成 dist[u]+1,进队。手推演练场(起点 1):

// 每行:出队 u → 新发现的邻居 → 出队后的队列 → 新写的 dist
出队 1   发现 2,3(dist=1)        队列: [2 3]      dist: 1:0 2:1 3:1
出队 2   发现 4(dist=2)          队列: [3 4]      dist: 4:2
出队 3   发现 5(dist=2)          队列: [4 5]      dist: 5:2
出队 4   发现 6(dist=3)          队列: [5 6]      dist: 6:3
出队 5   6 已发现(dist=3),跳过   队列: [6]
出队 6   4,5 都已发现,跳过        队列: [] → 结束
(7 号孤岛始终没被发现,dist[7] = -1)

关键性质:队列先进先出,所以 1 步的点(2、3)一定排在 2 步的点(4、5)前面被处理,2 步的又排在 3 步的(6)前面——队列天然按层排好了队,这就是"波纹一圈一圈"的代码实现。

考点:dist 在入队那一刻定下来,之后不再更新(首次到达已是最短,后来的提议只会更长或相等)。发现"已发现"的邻居直接跳过。
✅ 小检查:当队列第一次变成 [5 6] 时,dist[6] 是几?是谁"发现"它的?
看看答案
dist[6] = 3,是 4 号站发现它的(4 出队时 6 还是 -1,于是 dist[6] = dist[4]+1 = 3 并入队)。之后 5 出队时 6 已不是 -1,跳过。

🧩 子任务 3:BFS 代码

🎯 小目标:会写标准 BFS,说清 dist 为什么"只写一次"
// g 是邻接表;dist 初始全 -1
void bfs(int s) {
    queue<int> q;
    q.push(s);
    dist[s] = 0;                    // 起点入队、记账 0 步
    while (!q.empty()) {
        int u = q.front(); q.pop(); // 队头出队(先来的先服务)
        for (int v : g[u]) {
            if (dist[v] == -1) {    // 从没到过 → 首次到达 = 最短
                dist[v] = dist[u] + 1;  // 入队那一刻记账
                q.push(v);
            }
        }
    }
}

和 DFS 递归版对照着读:DFS 的"回溯"是函数 return,BFS 的"换圈"是队列 pop。DFS 用调用栈记住来路,BFS 用队列记住"下一圈名单"。dist 只在 dist[v] == -1 时写一次——这就是"首次到达即最短"的代码形式。

✅ 小检查:处理 u=4 时队列是 [5 6],下一个出队的是谁?如果图里有边 4—3,处理 u=4 时 3 会被重新记账吗?
看看答案
下一个出队的是 5(先进先出)。不会:3 的 dist 早被记成 1 了(1 的邻居),不是 -1,直接跳过——即使 4—3 这条边存在,3 也不会被改成 3 步。

🧩 子任务 4:DFS vs BFS——什么场景选谁

🎯 小目标:背下对比表,看到题目能 1 秒决定用哪种搜索
            DFS                          BFS
走法       一条路走到底,再回头            一圈一圈扩(按层)
数据结构   调用栈(递归)                  队列(先进先出)
回答       能不能到?有哪些点能到?          最少几步到?
找到的路   不一定是最近的                  一定是最短的(无权图)
典型题     连通块计数、迷宫找任一路径        最少步数、最少换乘、跳板问题
一句话     "走透一块"                      "先近后远"

判断口诀:题目问"最少几步 / 最近 / 最少换乘"→ BFS;问"能不能到 / 有几块 / 任意一条路"→ DFS 也行,BFS 也行。两种搜索都能数连通块(BFS 同样可以外层循环开新轮),但 BFS 的独门绝活是最短路,DFS 的独门绝活是走透(比如后续要学的深度信息、拓扑排序)。

✅ 小检查:题目"从 A 城到 B 城最少坐几趟车(每趟算 1 步)",用 DFS 还是 BFS?为什么?
看看答案
BFS。"最少几趟" = 最短步数,BFS 首次到达即最短;DFS 可能先绕一大圈找到 5 趟的路就停了,不一定是 3 趟的最优解。

💻 完整代码

同一张演练场图,BFS 从 1 出发,输出每个站到 1 的最短步数(-1 = 到不了)。

// 图 BFS:最短路(步数)
#include <bits/stdc++.h>
using namespace std;

int n;
vector<vector<int>> g;
vector<int> dist;

void bfs(int s) {
    queue<int> q;
    q.push(s);
    dist[s] = 0;
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int v : g[u]) {
            if (dist[v] == -1) {
                dist[v] = dist[u] + 1;
                q.push(v);
            }
        }
    }
}

int main() {
    n = 7;
    g.assign(n + 1, {});
    auto add = [&](int a, int b) { g[a].push_back(b); g[b].push_back(a); };
    add(1, 2); add(1, 3); add(2, 4); add(3, 5); add(4, 6); add(5, 6);
    // 7 号孤岛

    dist.assign(n + 1, -1);
    bfs(1);

    for (int i = 1; i <= n; i++)
        printf("1→%d: %d 步\n", i, dist[i]);
    // 输出:
    // 1→1: 0 步    1→2: 1 步    1→3: 1 步
    // 1→4: 2 步    1→5: 2 步    1→6: 3 步
    // 1→7: -1 步(孤岛,到不了)
    return 0;
}
易错点:① dist[v] = dist[u] + 1 必须在入队时写;写在"出队时"是常见错(同一个点可能被不同人提议,值会乱)。② dist 初始必须全 -1,起点单独置 0。③ 输出 -1 时要翻译成"不可达",别直接打印 -1 步。

✋ 你来试试

第 1 题:同一张图,BFS 起点改成 3,dist[6] 是几?给出最短路。
dist[6] = 2,最短路 3-5-6(或 3-1-2-4-6 是 4 步,不是最短)。推:3 的邻居 1、5 记 1 步;处理 5 时发现 6 记 2 步——首次到达即最短。
第 2 题:BFS 算出的 dist 数组跟代码怎么写有关吗?两个人写出的 dist 会不一样吗?
dist 的值唯一确定,跟实现无关:最短路长度是图本身的性质。可能不同的是"谁先发现某个点"(取决于邻居遍历顺序),但 dist 数值一定相同。
第 3 题:地铁题——"从 A 站到 B 站最少坐几站(每站算 1 步)",用 DFS 还是 BFS?写出关键两行。
BFS("最少"二字触发)。关键两行:if (dist[v] == -1) { dist[v] = dist[u] + 1; q.push(v); }——首次记账即最短步数。
🎯 一句话记住:BFS = 波纹一圈圈扩,先碰到的岸最近;队列排队 + dist 入队时记账;"最少几步"找 BFS,"走透/连通块"找 DFS。