CC++ & Algorithm

广度优先搜索(BFS)——像丢石头入水,一圈一圈扩散

困难10
语言版本:C++Python
概述:广度优先搜索从起点出发,一层一层向外扩张,先探索所有距离最近的点,再探索更远的点。

广度优先搜索(BFS)——像涟漪一样一层层扩散

想象你往平静的湖面丢下一颗石子,水波会一圈一圈向外扩散,先碰到最近的岸边,再缓缓扩散到更远的地方。广度优先搜索(BFS) 就是按照这种“同心圆”的方式工作的搜索算法:从起点出发,先检查所有“一步就能到”的点,再检查“两步才能到”的点,依此类推,直到找到目标或者把所有可达的点都访问完毕。

BFS 最擅长的就是解决 “从起点到终点最少需要几步” 的问题,比如:在迷宫里找最短路线、在社交网络中找两个人之间最少通过几个朋友认识、在地图上找从家到学校的最短路径……它就像个耐心的快递员,先送完所有距离近的客户,再送远一点的,保证每个包裹都按距离优先送达。


1. BFS 的核心思想:一层一层,不慌不忙

和它的好兄弟 深度优先搜索(DFS) 不同,DFS 喜欢一条路走到黑,撞到墙再回头;而 BFS 则像“地毯式排查”——先把起点周围所有方向都看一遍,记下来,然后逐个处理这些新位置,再继续看它们周围的新方向……这样一圈一圈地扩散,最先找到的目标往往就是距离最短的那个

生活中的比喻:传话游戏 假设班长接到一个紧急通知,需要告诉全班同学。他先把消息告诉身边的几个好朋友(一步距离),这些好朋友又分别告诉自己的朋友(两步距离),消息就这样一层层传开,绝不跳过任何人,也不会有人被重复通知。如果有同学同时被两个人通知,就只算一次。最终,每个同学都会在“最短的传话次数”内收到消息。

用队列来模拟“排队” 要实现这种“一层层”的效果,我们需要一个 队列(queue) 来记住待处理的任务。队列就像电影院买票的队伍:先排队的人先离开(先进先出)。在 BFS 中,我们先把起点放进队列,然后循环:每次从队首取出一个位置,处理它(比如输出或记录),再把它所有没去过的新邻居放进队尾。这样,先入队的邻居会先被处理,也就是离起点更近的点会先被访问,从而实现“一层一层”扩散。


2. 新手最容易犯的错误

  • 忘记把访问过的点标记起来
    如果访问了一个位置后不记录,后面可能会再次从其他路走过来,导致死循环。就像传话游戏中,如果已经告诉过小明,就不能再让另一个人再告诉他一遍,否则消息会反复传。

  • 忘记从队列中删除元素
    如果只 q.front() 而不 q.pop(),队列永远不为空,会无限循环。

  • 没有处理空节点或越界
    在树或图中,有些节点可能不存在(比如用 -1 表示空),如果不跳过,程序可能崩溃。

  • 忽略队列为空的情况
    如果在 while 循环外直接取队首,而队列可能为空,会导致错误。


3. 一步一步写代码:用 BFS 遍历一棵树

我们先复习一下树的结构。下面这棵树(用数组存储):

        0
       / \
      1   2
     / \ / \
    3  4 5  6
   /
  7

用 BFS 遍历的结果应该是:0 → 1 → 2 → 3 → 4 → 5 → 6 → 7(按层输出)。

#include <iostream>
#include <queue>        // 使用队列需要的头文件
using namespace std;

const int N = 10;       // 最多10个节点
// 左孩子数组,-1表示没有左孩子
int leftChild[N] = {1, 3, 5, 7, -1, -1, -1, -1, -1, -1};
// 右孩子数组,-1表示没有右孩子
int rightChild[N] = {2, 4, 6, -1, -1, -1, -1, -1, -1, -1};

void bfs(int start) {
    queue<int> q;               // 创建一个空队列,用来存放待处理的节点编号
    q.push(start);              // 把起点(根节点)放入队列
    while (!q.empty()) {        // 只要队列还有节点,就继续处理
        int node = q.front();   // 取出队列最前面的节点(最先被放进去的)
        q.pop();                // 把它从队列中删除(它已经被处理了)
        if (node == -1) continue;  // 如果节点是空的(-1),跳过它
        cout << node << " ";    // 访问当前节点,比如打印出来
        // 把它的左孩子和右孩子加入队列(如果存在),以后会按顺序处理
        q.push(leftChild[node]);   // 左孩子入队
        q.push(rightChild[node]);  // 右孩子入队
    }
}

int main() {
    cout << "BFS遍历结果: ";
    bfs(0);                     // 从根节点0开始遍历
    cout << endl;
    return 0;
}

运行结果:

BFS遍历结果: 0 1 2 3 4 5 6 7

看到区别了吗?BFS 先输出 0,然后输出它的两个孩子 1 和 2,然后输出 1 的孩子 3 和 4,再输出 2 的孩子 5 和 6,最后输出 3 的孩子 7。一层一层,整齐有序。


4. 另一个经典例子:用BFS走出迷宫(求最短步数)

假设有一个 4×4 的迷宫,0 表示空地可以走,1 表示墙不能走。起点在 (0,0),终点在 (3,3)。我们要用 BFS 找出从起点到终点最少需要走多少步。

迷宫地图:
0 0 1 0
0 0 0 0
0 1 0 1
0 0 0 0

BFS 会从起点开始,向上下左右四个方向扩散,每扩散一层就步数加 1。第一次到达终点时,那一步数就是最短路径长度。

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

const int ROWS = 4, COLS = 4;
// 迷宫:0可走,1墙
int maze[ROWS][COLS] = {
    {0, 0, 1, 0},
    {0, 0, 0, 0},
    {0, 1, 0, 1},
    {0, 0, 0, 0}
};
// 四个方向:上、下、左、右(用行和列的变化表示)
int dir_row[4] = {-1, 1, 0, 0}; // 行的变化
int dir_col[4] = {0, 0, -1, 1}; // 列的变化
// 记录每个位置是否已经访问过(初始化为false)
bool visited[ROWS][COLS] = {false};
// 记录从起点到每个位置的最短步数(初始化为0)
int step[ROWS][COLS] = {0};

int bfs(int sRow, int sCol, int eRow, int eCol) {
    // 队列里存的是坐标,可以用pair,也可以自己定义结构体
    queue<pair<int, int>> q;        // 队列每个元素是 (行,列)
    q.push({sRow, sCol});           // 起点入队
    visited[sRow][sCol] = true;     // 标记起点已访问
    step[sRow][sCol] = 0;           // 起点步数为0

    while (!q.empty()) {
        // 取出当前队首的坐标
        int cur_row = q.front().first;
        int cur_col = q.front().second;
        q.pop();                    // 删除队首

        // 检查是否到达终点
        if (cur_row == eRow && cur_col == eCol) {
            return step[cur_row][cur_col]; // 返回最短步数
        }

        // 尝试向四个方向走
        for (int i = 0; i < 4; i++) {
            int new_row = cur_row + dir_row[i];
            int new_col = cur_col + dir_col[i];

            // 检查新位置是否在迷宫内、不是墙、且没有访问过
            if (new_row >= 0 && new_row < ROWS &&
                new_col >= 0 && new_col < COLS &&
                maze[new_row][new_col] == 0 &&
                !visited[new_row][new_col]) {
                
                visited[new_row][new_col] = true;      // 标记已访问
                step[new_row][new_col] = step[cur_row][cur_col] + 1; // 步数增加1
                q.push({new_row, new_col});            // 新位置入队
            }
        }
    }
    return -1; // 如果队列空了还没到终点,说明无路可走
}

int main() {
    int result = bfs(0, 0, 3, 3);
    if (result != -1) {
        cout << "从起点到终点最少需要 " << result << " 步。\n";
    } else {
        cout << "无法到达终点。\n";
    }
    return 0;
}

运行结果:

从起点到终点最少需要 6 步。

(你可以自己画一画地图,验证一下这条最短路径是不是6步。)


5. 完整可运行的 BFS 综合示例(附注释)

下面是一个完整的、可以直接复制运行的代码,它包含了上面两个例子(树遍历和迷宫最短路径)以及一个简单的测试。你可以修改迷宫大小或树的结构,观察 BFS 的行为。

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

// 第一部分:BFS 遍历树(与上面一致)
void bfs_tree() {
    const int N = 10;
    int leftChild[N] = {1, 3, 5, 7, -1, -1, -1, -1, -1, -1};  // 左孩子
    int rightChild[N] = {2, 4, 6, -1, -1, -1, -1, -1, -1, -1}; // 右孩子

    queue<int> q;
    q.push(0);      // 根节点入队
    cout << "树 BFS: ";
    while (!q.empty()) {
        int node = q.front();
        q.pop();
        if (node == -1) continue;
        cout << node << " ";
        q.push(leftChild[node]);
        q.push(rightChild[node]);
    }
    cout << endl;
}

// 第二部分:迷宫最短路径
int bfs_maze() {
    const int R = 4, C = 4;
    int maze[R][C] = {
        {0, 0, 1, 0},
        {0, 0, 0, 0},
        {0, 1, 0, 1},
        {0, 0, 0, 0}
    };
    bool visited[R][C] = {false};
    int step[R][C] = {0};
    int dir_r[4] = {-1, 1, 0, 0}; // 上下左右
    int dir_c[4] = {0, 0, -1, 1};

    queue<pair<int, int>> q;
    q.push({0, 0});
    visited[0][0] = true;
    step[0][0] = 0;

    while (!q.empty()) {
        int r = q.front().first;
        int c = q.front().second;
        q.pop();

        // 到达终点
        if (r == 3 && c == 3) return step[r][c];

        for (int i = 0; i < 4; i++) {
            int nr = r + dir_r[i];
            int nc = c + dir_c[i];
            if (nr >= 0 && nr < R && nc >= 0 && nc < C &&
                maze[nr][nc] == 0 && !visited[nr][nc]) {
                visited[nr][nc] = true;
                step[nr][nc] = step[r][c] + 1;
                q.push({nr, nc});
            }
        }
    }
    return -1; // 无路
}

int main() {
    bfs_tree();                // 树遍历
    int steps = bfs_maze();    // 迷宫最短步数
    if (steps != -1)
        cout << "迷宫最短步数: " << steps << endl;
    else
        cout << "迷宫无解" << endl;
    return 0;
}

6. 什么时候用 BFS?记住这个口诀

问最短,用BFS;问所有,用DFS。

  • 如果问题要求 “最少步数”“最短时间” 或者 “最早到达”,那 BFS 就是第一选择。
  • 如果问题只要求 “是否存在路径” 或者 “有多少种可能”,DFS 和 BFS 都可以,但 DFS 代码更简洁(可以用递归)。
  • 如果问题要求 “按层处理”(比如打印二叉树的每一层),BFS 是自然选择。

7. 相关知识点指引

学完 BFS 后,你可以继续探索:

  • 深度优先搜索(DFS):BFS 的好兄弟,适合走迷宫找所有路径、判断连通性等。
  • 队列(queue):BFS 的核心数据结构,学明白队列的入队、出队、判空。
  • 图的表示:邻接矩阵、邻接表——当你处理更复杂的图时,需要选择合适的方式存储邻居。
  • 优先队列 / Dijkstra 算法:如果边带权重(比如路有长短),BFS 就不够用了,需要更高级的“最短路径算法”。
  • A 搜索*:在 BFS 基础上加上“启发式”思维,让搜索更快地找到目标。

BFS 是算法世界里最基础也最实用的工具之一,像涟漪一样,一圈一圈地帮助你找到答案。快打开编译器,自己写一个 BFS 试试吧!

例题精讲

1单选题

广度优先搜索(BFS)在遍历图时通常使用哪种数据结构来存储待访问的顶点?

A队列
B
C
D哈希表
2单选题

对于一个无权图,BFS从源点出发首次到达某个顶点时,所经过的路径一定是?

A最短路径
B最长路径
C任意路径
D唯一路径