8 宽度优先搜索

GESP C++ 六级:宽度优先搜索(BFS)

核心目标:理解 BFS 的“逐层扩展”思想,掌握队列实现与访问标记,熟练在树、图、网格上应用 BFS 求最短路径、连通块、层序信息,掌握多源 BFS 与双向 BFS 的基本思路,能辨析 BFS 与 DFS 的适用场景。
BFS 就是“一层一层往外扩”;用队列实现,先入先出;第一次到达目标时就是最短路径(无权图);树/图/网格是三大应用场景;多源 BFS 把所有起点一起入队,双向 BFS 从两端同时搜。

一、考纲定位

根据 CCF 官方考纲,GESP 六级明确要求掌握深度优先搜索(DFS) 和广度优先搜索(BFS) 的基本原理与实现,能够解决树、图、网格等结构上的搜索问题。

BFS 在六级考试中的考查分布:

考点 重要程度 典型考法
BFS 基本思想与队列实现 ★★★★★ 选择题、代码填空
树上的 BFS(层序遍历) 编程题、程序阅读
图上的 BFS(无权图最短路) 编程题
网格上的 BFS(迷宫最短路) 编程题高频
多源 BFS ★★★★☆ 编程题
双向 BFS(了解) ★★★☆☆ 选择题
BFS 与 DFS 的区别 ★★★★★ 选择题高频

二、BFS 基本概念

2.1 什么是 BFS

广度优先搜索(Breadth-First Search)是一种用于遍历或搜索树、图、网格等结构的算法。它的核心思想是:从起点开始,先访问所有距离为 1 的节点,再访问所有距离为 2 的节点,依此类推,像水波一样逐层向外扩散。

直观比喻:往平静的湖面扔一颗石子,波纹一圈一圈向外扩散,先到达近处,再到达远处。BFS 就是按“距离起点的层数”从小到大访问节点。

2.2 BFS 的核心要素

要素 作用 示例
队列 保证按层顺序访问 queue<状态> q
访问标记 防止重复入队 visited[] 数组
层数/步数 记录当前扩展到第几层 step 或 dist[]
终止条件 找到目标或队列为空 if (cur == target) return step

2.3 BFS 与 DFS 的区别

对比项 BFS DFS
数据结构 队列 栈 / 递归
遍历顺序 逐层扩展 一条路走到黑
最短路径 ✅ 保证(无权图) ❌ 不保证
空间复杂度 O(宽度) O(深度)
适用场景 最短路径、层序、多源扩散 连通性、回溯、排列组合

三、BFS 通用模板

3.1 队列实现模板

void bfs(起点) {
    queue<状态> q;
    q.push(起点);
    visited[起点] = true;      // 入队时标记

    while (!q.empty()) {
        状态 cur = q.front();
        q.pop();

        if (cur == 目标) {
            处理结果;
            return;
        }

        for (所有可能的下一步) {
            if (下一步合法 && !visited[下一步]) {
                visited[下一步] = true;   // 入队时标记,防止重复
                q.push(下一步);
            }
        }
    }
}

关键点:

  • 入队时标记,不是出队时标记,否则同一节点可能被多次入队。
  • 队列保证按层顺序,第一次到达目标时就是最短路径(无权图)。
  • 如果只求可达性,可以不记录步数;如果求最短步数,需要记录层数。

3.2 记录步数的模板

struct Node {
    int x, y, step;
};

int bfs(int sx, int sy, int ex, int ey) {
    queue<Node> q;
    q.push({sx, sy, 0});
    visited[sx][sy] = true;

    while (!q.empty()) {
        Node cur = q.front(); q.pop();

        if (cur.x == ex && cur.y == ey) {
            return cur.step;      // 第一次到达即最短
        }

        for (int i = 0; i < 4; i++) {
            int nx = cur.x + dx[i];
            int ny = cur.y + dy[i];
            if (合法 && !visited[nx][ny]) {
                visited[nx][ny] = true;
                q.push({nx, ny, cur.step + 1});
            }
        }
    }
    return -1;   // 无法到达
}

四、树上的 BFS

4.1 树的层序遍历

void levelOrder(TreeNode* root) {
    if (!root) return;
    queue<TreeNode*> q;
    q.push(root);

    while (!q.empty()) {
        TreeNode* cur = q.front(); q.pop();
        cout << cur->val << " ";

        if (cur->left) q.push(cur->left);
        if (cur->right) q.push(cur->right);
    }
}

4.2 按层输出

void levelOrderByLevel(TreeNode* root) {
    if (!root) return;
    queue<TreeNode*> q;
    q.push(root);

    while (!q.empty()) {
        int sz = q.size();          // 当前层的节点数
        for (int i = 0; i < sz; i++) {
            TreeNode* cur = q.front(); q.pop();
            cout << cur->val << " ";
            if (cur->left) q.push(cur->left);
            if (cur->right) q.push(cur->right);
        }
        cout << endl;               // 每层结束换行
    }
}

4.3 求树的最小深度

int minDepth(TreeNode* root) {
    if (!root) return 0;
    queue<pair<TreeNode*, int>> q;
    q.push({root, 1});

    while (!q.empty()) {
        auto [node, depth] = q.front(); q.pop();
        if (!node->left && !node->right) return depth;
        if (node->left) q.push({node->left, depth + 1});
        if (node->right) q.push({node->right, depth + 1});
    }
    return 0;
}

五、图上的 BFS

5.1 无权图最短路径

int bfs(int start, int target, int n) {
    vector<int> dist(n + 1, -1);
    queue<int> q;
    q.push(start);
    dist[start] = 0;

    while (!q.empty()) {
        int u = q.front(); q.pop();
        if (u == target) return dist[u];

        for (int v : adj[u]) {
            if (dist[v] == -1) {
                dist[v] = dist[u] + 1;
                q.push(v);
            }
        }
    }
    return -1;
}

5.2 连通分量个数

int countComponents(int n) {
    vector<bool> visited(n + 1, false);
    int cnt = 0;

    for (int i = 1; i <= n; i++) {
        if (!visited[i]) {
            queue<int> q;
            q.push(i);
            visited[i] = true;
            while (!q.empty()) {
                int u = q.front(); q.pop();
                for (int v : adj[u]) {
                    if (!visited[v]) {
                        visited[v] = true;
                        q.push(v);
                    }
                }
            }
            cnt++;
        }
    }
    return cnt;
}

六、网格上的 BFS(迷宫最短路)

6.1 方向数组

int dx[] = {-1, 1, 0, 0};
int dy[] = {0, 0, -1, 1};

6.2 迷宫最短步数

struct Node { int x, y, step; };

int bfs(int sx, int sy, int ex, int ey) {
    queue<Node> q;
    q.push({sx, sy, 0});
    vis[sx][sy] = true;

    while (!q.empty()) {
        Node cur = q.front(); q.pop();

        if (cur.x == ex && cur.y == ey) return cur.step;

        for (int i = 0; i < 4; i++) {
            int nx = cur.x + dx[i];
            int ny = cur.y + dy[i];
            if (nx >= 0 && nx < n && ny >= 0 && ny < m &&
                !vis[nx][ny] && grid[nx][ny] != '#') {
                vis[nx][ny] = true;
                q.push({nx, ny, cur.step + 1});
            }
        }
    }
    return -1;
}

6.3 岛屿数量(BFS 版)

void bfs(int x, int y) {
    queue<pair<int, int>> q;
    q.push({x, y});
    vis[x][y] = true;

    while (!q.empty()) {
        auto [cx, cy] = q.front(); q.pop();
        for (int i = 0; i < 4; i++) {
            int nx = cx + dx[i], ny = cy + dy[i];
            if (nx >= 0 && nx < n && ny >= 0 && ny < m &&
                !vis[nx][ny] && grid[nx][ny] == '1') {
                vis[nx][ny] = true;
                q.push({nx, ny});
            }
        }
    }
}

七、多源 BFS

7.1 什么是多源 BFS

多源 BFS 是指有多个起点同时开始扩展的 BFS。将所有起点一起入队,然后正常进行 BFS,就能得到每个位置到“最近起点”的距离。

7.2 经典问题:腐烂的橘子

问题:网格中 0 表示空,1 表示新鲜橘子,2 表示腐烂橘子。每分钟腐烂橘子会感染上下左右的新鲜橘子,求所有橘子腐烂的最少分钟数。

int orangesRotting(vector<vector<int>>& grid) {
    int n = grid.size(), m = grid[0].size();
    queue<pair<int, int>> q;
    int fresh = 0;

    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++) {
            if (grid[i][j] == 2) q.push({i, j});
            else if (grid[i][j] == 1) fresh++;
        }

    if (fresh == 0) return 0;

    int minutes = 0;
    int dx[] = {-1, 1, 0, 0};
    int dy[] = {0, 0, -1, 1};

    while (!q.empty()) {
        int sz = q.size();
        bool spread = false;
        for (int k = 0; k < sz; k++) {
            auto [x, y] = q.front(); q.pop();
            for (int i = 0; i < 4; i++) {
                int nx = x + dx[i], ny = y + dy[i];
                if (nx >= 0 && nx < n && ny >= 0 && ny < m && grid[nx][ny] == 1) {
                    grid[nx][ny] = 2;
                    q.push({nx, ny});
                    fresh--;
                    spread = true;
                }
            }
        }
        if (spread) minutes++;
    }
    return fresh == 0 ? minutes : -1;
}

关键点:所有腐烂橘子一开始都入队,逐层向外扩散,层数就是分钟数。

八、双向 BFS(了解)

8.1 核心思想

双向 BFS 从起点和终点同时开始 BFS,当两个搜索相遇时,就找到了最短路径。适合状态空间较大、但起点和终点都已知的场景。

8.2 基本框架

int biBFS(int start, int target) {
    queue<int> q1, q2;
    unordered_map<int, int> dist1, dist2;
    q1.push(start); dist1[start] = 0;
    q2.push(target); dist2[target] = 0;

    while (!q1.empty() && !q2.empty()) {
        // 扩展较小的一边
        if (q1.size() > q2.size()) swap(q1, q2), swap(dist1, dist2);

        int sz = q1.size();
        for (int i = 0; i < sz; i++) {
            int u = q1.front(); q1.pop();
            for (int v : adj[u]) {
                if (dist1.count(v)) continue;
                dist1[v] = dist1[u] + 1;
                if (dist2.count(v)) return dist1[v] + dist2[v];
                q1.push(v);
            }
        }
    }
    return -1;
}

六级对双向 BFS 要求较低,了解思想即可。

九、GESP 六级 BFS 真题精讲

真题1:迷宫最短路径(编程题)

给定 n×m 的迷宫,# 是墙,. 是路,求从 S 到 E 的最短步数。

#include <iostream>
#include <queue>
using namespace std;

struct Node { int x, y, step; };
int n, m;
char grid[105][105];
bool vis[105][105];
int dx[] = {-1, 1, 0, 0};
int dy[] = {0, 0, -1, 1};

int main() {
    cin >> n >> m;
    int sx, sy, ex, ey;
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++) {
            cin >> grid[i][j];
            if (grid[i][j] == 'S') sx = i, sy = j;
            if (grid[i][j] == 'E') ex = i, ey = j;
        }

    queue<Node> q;
    q.push({sx, sy, 0});
    vis[sx][sy] = true;

    while (!q.empty()) {
        Node cur = q.front(); q.pop();
        if (cur.x == ex && cur.y == ey) {
            cout << cur.step << endl;
            return 0;
        }
        for (int i = 0; i < 4; i++) {
            int nx = cur.x + dx[i], ny = cur.y + dy[i];
            if (nx >= 0 && nx < n && ny >= 0 && ny < m &&
                !vis[nx][ny] && grid[nx][ny] != '#') {
                vis[nx][ny] = true;
                q.push({nx, ny, cur.step + 1});
            }
        }
    }
    cout << -1 << endl;
    return 0;
}

真题2:BFS 与 DFS 区别(选择题)

以下关于 BFS 和 DFS 的说法,正确的是( )。
A. BFS 用栈实现,DFS 用队列实现
B. BFS 适合求无权图最短路径,DFS 不保证最短路径
C. BFS 的空间复杂度是 O(深度),DFS 是 O(宽度)
D. BFS 只能用于树,不能用于图
答案:B

真题3:多源 BFS(程序阅读)

以下代码实现了什么功能?

queue<pair<int,int>> q;
for (int i = 0; i < n; i++)
 for (int j = 0; j < m; j++)
     if (grid[i][j] == 2) q.push({i, j});
while (!q.empty()) { ... }

答案:多源 BFS,所有腐烂橘子同时向外扩散。

真题4:BFS 代码填空

下面 BFS 代码中,横线处应填入( )。

while (!q.empty()) {
 int u = q.front(); q.pop();
 for (int v : adj[u]) {
     if (!visited[v]) {
         visited[v] = true;
         ______;
     }
 }
}

答案:q.push(v);

十、易错点汇总

  1. 入队时不标记,出队时才标记:导致同一节点多次入队,效率降低甚至死循环。
  2. 忘记判空就取队头:q.front() 前必须 !q.empty()。
  3. 步数更新位置错误:步数在入队时更新为 cur.step + 1,不是出队时。
  4. 网格 BFS 忘记边界检查:nx >= 0 && nx < n && ny >= 0 && ny < m 不能漏。
  5. 多源 BFS 只入队一个起点:所有起点必须一起入队。
  6. BFS 求最短路用于带权图:BFS 只保证无权图最短路,带权图需要用 Dijkstra。
  7. 队列中存状态太大:可以只存坐标,步数用 dist 数组记录。
  8. 层序遍历忘记记录当前层大小:按层输出时需要 int sz = q.size()。
  9. BFS 与 DFS 混淆:BFS 用队列,DFS 用栈/递归。
  10. 双向 BFS 交换队列时忘记交换距离数组:swap(q1, q2), swap(dist1, dist2)。

十一、速查表

需求 写法 关键注意
BFS 模板 queue; push(起点); 标记; while (!empty) { pop; 遍历邻居; 标记+入队; } 入队时标记
记录步数 q.push({nx, ny, cur.step + 1}) 入队时更新步数
树层序遍历 queue<TreeNode*>; push(root); while { pop; 左右入队; } 按层输出需 sz
图最短路 dist[] 数组 + 队列 无权图
网格 BFS 方向数组 + 边界检查 入队时标记
多源 BFS 所有起点一起入队 层数即扩散时间
双向 BFS 两端同时 BFS,相遇即最短 扩展较小的一边
BFS vs DFS BFS 队列逐层,DFS 栈/递归深入 BFS 保证无权最短路

十二、总结

宽度优先搜索是 GESP 六级搜索算法的核心内容。

  1. BFS 的核心是“逐层扩展”,用队列实现,先入先出。
  2. 入队时就要标记已访问,防止重复入队。
  3. BFS 第一次到达目标时就是最短路径(无权图)。
  4. 树上的 BFS 就是层序遍历,按层输出需记录当前层大小。
  5. 图上的 BFS 可求无权图最短路径和连通分量。
  6. 网格 BFS 用方向数组 + 边界检查,是迷宫最短路的标准解法。
  7. 多源 BFS 将所有起点一起入队,适合“腐烂橘子”类扩散问题。
  8. 双向 BFS 从起点和终点同时搜索,适合状态空间大的场景。
  9. BFS 不适用于带权图最短路,带权图要用 Dijkstra。
  10. 多练“迷宫最短路”“层序遍历”“多源 BFS”“连通分量”四类题。

复习建议:先把 BFS 通用模板和网格 BFS 模板写熟,确保闭卷能默写;再练习树上的层序遍历和按层输出;然后掌握多源 BFS 的入队方式;最后用真题做限时训练。考试中先判断题目考的是树、图还是网格,再确定用单源还是多源 BFS,最后检查标记、边界和步数更新。