NOI C++ 课程体系 / 算法篇 / 第14讲
CSP-J 入门级 · 搜索算法 · m4-14

广度优先搜索 BFS

一层一层向外扩散,像水波纹一样搜索 —— 掌握 BFS 的核心思想,学会用队列实现图的广度优先遍历,求解最短路径和连通性问题。

6 大模块 3 大核心要素 3 个经典案例 5 大易错点 12+20 道练习
本讲 · 第 38/56 讲广度优先搜索 BFS
模块 01

情景导航

用生活中的"扩散"场景,直觉理解 BFS 的核心思想。

故事火灾逃生:一层层找到最近的安全出口

一栋大楼发生了火灾,你站在第 3 层的 305 房间。消防员需要从你所在的位置出发,逐层搜索所有被困人员。

消防员的搜索策略是:

第1步:先搜索 305 房间(距离 0)
第2步:搜索与 305 直接相邻的所有房间 — 304、306、楼梯口(距离 1)
第3步:搜索距离为 2 的房间 — 303、307、2 楼楼梯口、4 楼楼梯口(距离 2)
第4步:搜索距离为 3 的房间...以此类推

一层一层向外扩散,先近后远!

这种搜索方式有一个重要性质:第一次到达某个房间时,走过的路径一定是最短的。因为 BFS 先搜索近处,后搜索远处。

BFS = 一层一层扩散,第一次到达就是最短路径!

💡 BFS 的核心特征

① 使用队列(Queue):先进先出,保证先发现的节点先被处理

② 层次遍历:先访问距离为 1 的所有节点,再访问距离为 2 的,以此类推

③ 标记已访问:防止重复访问,用 visited 数组记录

④ 最短路径保证:在无权图中,BFS 第一次到达终点的路径一定是最短路径

①生活类比:BFS 无处不在
🌊

水波纹扩散

石头丢入水中,波纹一圈一圈向外扩散。

→ 层次扩散
📡

WiFi 信号传播

路由器信号从近到远,一层一层覆盖。

→ 最短距离
🦠

传染病扩散

感染者先传染身边密切接触者,再扩散到更远的人。

→ 逐层感染
🗺️

导航找最短路线

地图导航从起点开始,逐层搜索最近的路口。

→ 最短路径
②编程定义

广度优先搜索(BFS, Breadth-First Search):从起始节点出发,先访问所有与起点距离为 1 的节点,再访问距离为 2 的节点,依次类推,直到访问完所有可达节点。使用队列(FIFO)来实现。

BFS 是图论中最基础的搜索算法之一,在 CSP-J 中常用于求解无权图最短路径、连通分量、层级关系等问题。

📌 本讲核心知识地图

板块核心内容对应大纲
BFS 三要素队列、访问标记、层次扩展搜索算法基础
迷宫最短路径网格BFS + 方向数组经典BFS应用
洪水填充连通块计数与标记经典BFS应用
图的BFS遍历邻接表 + 层次遍历图论基础
BFS vs DFS队列 vs 栈,最短 vs 全搜算法对比
③核心口诀
🔑 BFS 三字诀

入队起始点,标记已访问;
出队取队首,扩展邻居入;
层层向外扩,先到即最短!

🔑 BFS vs DFS 一句话

BFS 像水波纹 —— 一层一层扩散,找最短路径;
DFS 像走迷宫 —— 一条路走到黑,搜索全部方案;
求最短用 BFS,求全部/存在性用 DFS!

模块 02

知识探究

深入理解 BFS 的核心机制,通过三个经典案例掌握实战技巧。

1BFS 三要素 核心

任何一个 BFS 实现都包含以下三个要素:

要素① 队列 Queue
  • 先进先出(FIFO)
  • 存"待扩展"的节点
  • C++ 用 queue<int>
  • 保证层次顺序
要素② 访问标记 visited
  • 防止重复访问
  • bool visited[N]={false}
  • 入队时立即标记!
  • 不标记 → 死循环
要素③ 层次扩展
  • 每次取出队首节点
  • 遍历其所有未访问邻居
  • 邻居入队并标记
  • 记录距离 dist[]
🔑 BFS 的核心保证

在无权图(每条边权重为1)中,BFS 第一次到达某个节点时,经过的边数一定是最少的。
因此 BFS 天然适合求最短路径、最少步数、最短距离等问题。
如果边有权重,则需要 Dijkstra 或 Bellman-Ford 算法。

2BFS 通用模板 必背
C++bfs_template.cpp — 图的BFS通用模板
#include <iostream>
#include <queue>
#include <vector>
#include <cstring>
using namespace std;

const int MAXN = 1005;
vector<int> adj[MAXN];  // 邻接表
bool visited[MAXN];      // 访问标记
int dist[MAXN];           // 到起点的距离

void bfs(int start) {
    queue<int> q;
    q.push(start);            // ① 起始点入队
    visited[start] = true;    // ② 标记已访问
    dist[start] = 0;          // ③ 起点距离为0

while (!q.empty()) {
        int u = q.front();     // 取队首
        q.pop();               // 出队

for (int v : adj[u]) {  // 遍历所有邻居
            if (!visited[v]) {
                visited[v] = true;  // 标记
                dist[v] = dist[u] + 1;  // 距离+1
                q.push(v);          // 入队
            }
        }
    }
}

执行过程可视化(从节点1出发的BFS):

初始: 1 → 队列 [1]
第1层: 2 3 4 → 1出队,邻居2,3,4入队
第2层: 3 4 5 6 → 2出队,邻居5,6入队
第3层: 4 5 6 7 → 3出队,邻居7入队
节点1234567
dist0111222
访问顺序1st2nd3rd4th5th6th7th
案例经典案例1:迷宫最短路径 高频

给定一个 n×m 的迷宫,0 表示空地(可走),1 表示墙壁(不可走)。从起点 (sx, sy) 到终点 (ex, ey) 的最短步数是多少?

S0
01
1
05
07
09
02
03
06
1
08
010
1
1
04
011
012
E13
014
015
016
1
1
1
4×6 迷宫 | S=(0,0), E=(2,5) | 数字为BFS访问顺序 | 最短路径=7步(黄色)
C++maze_bfs.cpp — 迷宫最短路径
#include <iostream>
#include <queue>
#include <cstring>
using namespace std;

int n, m;
int grid[105][105];
int dist[105][105];
// 方向数组:上、下、左、右
int dx[] = {-1, 1, 0, 0};
int dy[] = {0, 0, -1, 1};

int bfs(int sx, int sy, int ex, int ey) {
    memset(dist, -1, sizeof(dist));
    queue<pair<int,int>> q;
    q.push({sx, sy});
    dist[sx][sy] = 0;

while (!q.empty()) {
        auto [x, y] = q.front(); q.pop();
        if (x == ex && y == ey) return dist[x][y];

for (int d = 0; d < 4; d++) {
            int nx = x + dx[d], ny = y + dy[d];
            if (nx>=0 && nx<n && ny>=0 && ny<m
                && grid[nx][ny]==0 && dist[nx][ny]==-1) {
                dist[nx][ny] = dist[x][y] + 1;
                q.push({nx, ny});
            }
        }
    }
    return -1; // 不可达
}

int main() {
    cin >> n >> m;
    for (int i=0;i<n;i++)
        for (int j=0;j<m;j++) cin >> grid[i][j];
    int sx,sy,ex,ey;
    cin >> sx >> sy >> ex >> ey;
    cout << bfs(sx, sy, ex, ey) << endl;
    return 0;
}
运行结果
输入:4 6
0 0 1 0 0 0
0 0 0 1 0 0
1 1 0 0 0 0
0 0 0 1 1 1
0 0 2 5
输出:13

💡 网格BFS的关键技巧

方向数组:用 dx[]={−1,1,0,0}, dy[]={0,0,−1,1} 表示上下左右四个方向

边界检查:nx>=0 && nx=0 && ny

访问标记:用 dist 数组同时充当"距离记录"和"访问标记"(初始值 -1 表示未访问)

提前终止:到达终点时直接返回,不需要搜索完整个图

时间复杂度:O(n×m),每个格子最多访问一次。

案例经典案例2:洪水填充(Flood Fill) 经典

给定一个 n×m 的地图,1 表示陆地,0 表示海洋。相连的陆地(上下左右)构成一个岛屿。问共有多少个岛屿?

🔍 BFS 思路分析

① 遍历地图的每个格子

② 如果找到一个未访问的陆地(1),就从它开始做一次 BFS

③ BFS 会把与它相连的所有陆地都标记为已访问

④ 每次启动 BFS 就发现一个新的岛屿,计数器 +1

总岛屿数 = 启动 BFS 的次数

C++flood_fill.cpp — 岛屿计数
#include <iostream>
#include <queue>
using namespace std;

int n, m;
int grid[105][105];
bool vis[105][105];
int dx[]={-1,1,0,0}, dy[]={0,0,-1,1};

void bfs(int sx, int sy) {
    queue<pair<int,int>> q;
    q.push({sx, sy});
    vis[sx][sy] = true;
    while (!q.empty()) {
        auto [x,y] = q.front(); q.pop();
        for (int d=0;d<4;d++) {
            int nx=x+dx[d], ny=y+dy[d];
            if (nx>=0&&&nx<n&&ny>=0&&ny<m
                &&grid[nx][ny]==1&&!vis[nx][ny]) {
                vis[nx][ny] = true;
                q.push({nx, ny});
            }
        }
    }
}

int main() {
    cin >> n >> m;
    for(int i=0;i<n;i++)
        for(int j=0;j<m;j++) cin >> grid[i][j];

int islands = 0;
    for(int i=0;i<n;i++)
        for(int j=0;j<m;j++)
            if(grid[i][j]==1 && !vis[i][j]) {
                bfs(i, j);
                islands++;
            }
    cout << islands << endl;
    return 0;
}
运行结果
输入:5 5
1 1 0 0 0
1 1 0 0 1
0 0 1 0 1
0 0 1 1 0
0 0 0 0 0
输出:3

💡 洪水填充的本质

洪水填充(Flood Fill)= 找连通块。每次 BFS 从一块未访问的陆地出发,把与它相连的所有陆地都标记为已访问。

启动 BFS 的次数 = 连通块的数量 = 岛屿数

变体:如果允许对角线相连(8方向),只需把 dx/dy 数组扩展到 8 个方向即可。

案例经典案例3:图的BFS遍历与二分图判定 经典

给定一个无向图,用 BFS 遍历所有节点,并判断该图是否为二分图(能否将所有节点分成两组,使得每条边的两个端点分别属于不同组)。

🔍 BFS 染色法判定二分图

① 从任意未染色节点开始,染成颜色 1

② BFS 扩展时,将邻居染成相反颜色(3-color → 1-color 或反之)

③ 如果发现某个邻居已经被染色,且颜色与当前节点相同,则不是二分图

BFS 天然适合染色,因为它按层次扩展,相邻层恰好是不同颜色!

C++bfs_bipartite.cpp — 二分图判定
#include <iostream>
#include <vector>
#include <queue>
using namespace std;

int n, m;
vector<int> adj[1005];
int color[1005]; // 0=未染色, 1=颜色A, 2=颜色B

bool bfs(int start) {
    queue<int> q;
    q.push(start);
    color[start] = 1;

while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int v : adj[u]) {
            if (color[v] == 0) {
                color[v] = 3 - color[u]; // 染相反色
                q.push(v);
            } else if (color[v] == color[u]) {
                return false; // 冲突!不是二分图
            }
        }
    }
    return true;
}

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);
    }

bool ok = true;
    for (int i=1;i<=n;i++)
        if (color[i]==0 && !bfs(i))
            { ok = false; break; }

cout << (ok ? "是二分图" : "不是二分图") << endl;
    return 0;
}
运行结果
输入1:4 4      输入2:3 3
1 2             1 2
2 3             2 3
3 4             3 1
4 1
输出:是二分图    输出:不是二分图
⚡BFS vs DFS 全面对比 辨析
对比维度BFS(广度优先)DFS(深度优先)
数据结构队列(Queue, FIFO)栈(Stack, LIFO)/ 递归
搜索方式一层一层扩散(水波纹)一条路走到黑(走迷宫)
最短路径✅ 无权图保证最短❌ 不保证最短
空间复杂度O(宽度) — 可能较大O(深度) — 通常较小
适用场景最短路径、层序遍历、连通块全排列、回溯、拓扑排序
代码风格循环 + queue递归 或 循环 + stack
竞赛口诀"求最短,用BFS""求全部,用DFS"
✅ BFS 适合的问题
  • 无权图最短路径
  • 迷宫最少步数
  • 连通块计数(洪水填充)
  • 层序遍历(树的BFS)
  • 最少操作次数
✅ DFS 适合的问题
  • 全排列 / 组合枚举
  • 路径搜索(所有路径)
  • 连通性判断
  • 拓扑排序
  • 回溯(八皇后等)
模块 03

实践应用

通过分析和编程题,巩固 BFS 的实战能力。

题1BFS 过程追踪

题目:给定以下 4×4 迷宫,起点 S=(0,0),终点 E=(3,3)。0=空地,1=墙壁。请手动模拟 BFS 过程,写出每步队列状态和最短路径长度。

0 0 0 1
1 0 1 0
0 0 0 0
0 1 0 0
查看参考答案

逐步模拟 BFS:

初始:队列 [(0,0)],dist[0][0]=0

出队 (0,0):邻居 (0,1)✓ (1,0)✗墙壁 → 入队 [(0,1)],dist[0][1]=1

出队 (0,1):邻居 (0,0)已访问 (0,2)✓ (1,1)✓ → 入队 [(0,2),(1,1)],dist=2

出队 (0,2):邻居 (0,1)已访问 (0,3)✗墙 (1,2)✗墙 → 无新入队

出队 (1,1):邻居 (0,1)已访问 (1,0)✗ (1,2)✗ (2,1)✓ → 入队 [(2,1)],dist=3

出队 (2,1):邻居 (1,1)已访问 (2,0)✓ (2,2)✓ (3,1)✗墙 → 入队 [(2,0),(2,2)],dist=4

出队 (2,0):邻居 (1,0)✗ (2,1)已访问 (3,0)✓ → 入队 [(2,2),(3,0)],dist=5

出队 (2,2):邻居 (1,2)✗ (2,1)已访问 (2,3)✓ (3,2)✓ → 入队 [(3,0),(2,3),(3,2)],dist=5

出队 (3,0):邻居 (2,0)已访问 (3,1)✗ → 无新入队

出队 (2,3):邻居 (1,3)✓ (2,2)已访问 (3,3)✓ → 入队 [(3,2),(1,3),(3,3)],dist=6

出队 (3,2):邻居 (2,2)已访问 (3,1)✗ (3,3)✓ → (3,3)已入队,不重复

出队 (3,3):到达终点!dist[3][3] = 6

✅ 最短路径长度 = 6 步

路径:(0,0)→(0,1)→(1,1)→(2,1)→(2,2)→(2,3)→(3,3)

题2最短距离计算

题目:在一个 n×n 的棋盘上,骑士(马)从 (0,0) 出发,按照国际象棋中马的走法(日字形),问到达 (ex, ey) 的最少步数。n=8, 终点=(7,7)。

提示:马有 8 个方向可走:(±1,±2) 和 (±2,±1)。这就是一个网格BFS,方向数组有 8 个元素。
查看参考答案
#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
int n;
int dist[105][105];
// 马的8个方向
int dx[]={-2,-2,-1,-1,1,1,2,2};
int dy[]={-1,1,-2,2,-2,2,-1,1};
int bfs(int sx,int sy,int ex,int ey){
    memset(dist,-1,sizeof(dist));
    queue<pair<int,int>> q;
    q.push({sx,sy}); dist[sx][sy]=0;
    while(!q.empty()){
        auto [x,y]=q.front(); q.pop();
        if(x==ex&&y==ey) return dist[x][y];
        for(int d=0;d<8;d++){
            int nx=x+dx[d],ny=y+dy[d];
            if(nx>=0&&nx<n&&ny>=0&&ny<n&&dist[nx][ny]==-1){
                dist[nx][ny]=dist[x][y]+1;
                q.push({nx,ny});
            }
        }
    }
    return -1;
}
int main(){
    n=8;
    cout<<bfs(0,0,7,7)<<endl;
    return 0;
}
运行结果
6

分析:马从 (0,0) 到 (7,7) 最少需要 6 步。这是典型的网格BFS,只是方向数组从 4 个变成了 8 个。

题3连通块计数

题目:给定一个 5×5 的 01 矩阵,1 表示黑色像素,0 表示白色像素。相连的黑色像素(上下左右)构成一个"黑色区域"。问有多少个互不相连的黑色区域?

1 1 0 1 0
1 0 0 1 0
0 0 1 0 1
0 1 1 0 1
0 0 0 0 1
查看参考答案

手动分析连通块:

块1(左上角):(0,0)(0,1)(1,0) — 3个像素相连

块2(右上):(0,3)(1,3) — 2个像素相连

块3(中间):(2,2)(3,1)(3,2) — 3个像素相连

块4(右侧):(2,4)(3,4)(4,4) — 3个像素相连

✅ 共 4 个黑色区域(连通块)

编程实现:就是案例2的洪水填充,直接套用模板即可。

模块 04

总结提升

1核心要点总结
知识板块必须记住的核心
BFS 三要素队列 + 访问标记 + 层次扩展
核心保证无权图中 BFS 第一次到达 = 最短路径
网格BFS方向数组 dx/dy + 边界检查 + dist标记
洪水填充BFS 启动次数 = 连通块数量
二分图判定BFS 染色法:邻居染相反色,冲突则不是
时间复杂度O(V+E),每个节点和边各访问一次
BFS vs DFS求最短 → BFS;求全部/存在性 → DFS
2拓展:双向BFS优化

💡 双向BFS:从两端同时搜索

当搜索空间很大时,单向 BFS 需要扩展大量节点。双向BFS同时从起点和终点出发,当两个搜索"相遇"时即找到最短路径。

原理:单向 BFS 搜索半径 r 的圆面积 = πr²;双向 BFS 各搜 r/2,总面积 = 2×π(r/2)² = πr²/2,减少一半!

实现要点:用两个队列分别从起点和终点搜索,当一个队列扩展到的节点已被另一个队列访问过时,路径长度 = 两端距离之和。

适用场景:已知起点和终点的最短路径问题(如八数码、字串变换等)。

3思考与启发

💡 学完本讲,你应该掌握的6项能力

1. 能写出 BFS 的通用模板(队列 + 访问标记 + 层次扩展)。

2. 能用方向数组实现网格BFS(迷宫最短路径)。

3. 能用洪水填充计数连通块(岛屿问题)。

4. 能用 BFS 染色法判定二分图。

5. 能说出 BFS 和 DFS 的区别和各自适用场景。

6. 知道 BFS 保证无权图最短路径的原理。

模块 05

学习检测

5 个初学者最常犯的错误。

易错1

入队时不标记 visited

❌ 错误
出队时才标记 visited,导致同一节点被多次入队,队列爆炸。
✅ 正确
入队时立即标记 visited[v]=true,防止重复入队。
口诀:入队即标记,出队不标记!
易错2

网格BFS边界检查遗漏

❌ 错误
只检查了 nx>=0 和 nx=0 和 ny
✅ 正确
四个方向都要检查:nx>=0 && nx=0 && ny,缺一不可。
口诀:边界四检查,越界就完蛋!
易错3

用 DFS 求最短路径

❌ 错误
"DFS 也能找到路径,就用 DFS 求最短路径吧。"
✅ 正确
DFS 找到的第一条路径不一定是最短的。求无权图最短路径必须用 BFS。
口诀:求最短用 BFS,求全搜用 DFS!
易错4

忘记处理不可达情况

❌ 错误
BFS 结束后直接输出 dist[ex][ey],但终点不可达时 dist 为 -1。
✅ 正确
BFS 结束后检查 dist[ex][ey] 是否为 -1,如果是则输出"不可达"或题目要求的特殊值。
口诀:BFS 完了查 dist,-1 就是到不了!
易错5

多源BFS只设一个起点

❌ 错误
"从每个起点分别做BFS",多次 BFS 效率低。
✅ 正确
多源BFS:把所有起点同时入队,一次 BFS 搞定!如"腐烂的橘子"问题。
口诀:多源一起入队,一次BFS全搞定!
速查BFS 速查表
知识点关键结论
数据结构队列 queue(FIFO)
访问标记入队时立即标记 visited
最短路径无权图 BFS 第一次到达 = 最短
方向数组dx[]={-1,1,0,0}, dy[]={0,0,-1,1}
洪水填充BFS 启动次数 = 连通块数
二分图判定BFS 染色,邻居染相反色
复杂度时间 O(V+E),空间 O(V)
多源BFS多个起点同时入队
📌知识点笔记卡

🌊 BFS 核心

一层一层扩散

队列 + 标记 + 扩展

先到即最短

🗺️ 迷宫BFS

方向数组 dx/dy

边界四检查

dist 同时当标记

🏝️ 洪水填充

找连通块

BFS次数=岛屿数

经典模板题

🎨 二分图判定

BFS 染色法

邻居染相反色

冲突=不是二分图

⚡ BFS vs DFS

求最短 → BFS

求全部 → DFS

队列 vs 栈/递归

🚀 多源BFS

多起点同时入队

一次搜索全搞定

腐烂橘子模型

课后练习

分层练习作业

A基础 / B提高 / C挑战,难度递增,点击展开答案与解析。

🟢 A组 · 基础(4题)

A1选择题

广度优先搜索(BFS)使用的数据结构是()

A. 栈   B. 队列   C. 堆   D. 二叉树

查看参考答案

答案:B. 队列

BFS 使用队列(FIFO,先进先出)来保证先发现的节点先被扩展,从而实现层次遍历。DFS 使用栈(或递归)。

考点:BFS 用队列(FIFO)保证先发现的节点先被扩展,从而实现按层遍历。

易错:把 BFS 与 DFS 使用的数据结构记反,进而把扩展顺序也推错。

A2选择题

在无权图中,BFS 第一次到达终点时走过的路径()

A. 不一定是最短路径   B. 一定是最短路径   C. 一定是最长路径   D. 无法确定

查看参考答案

答案:B. 一定是最短路径

BFS 按层次扩展,先搜索距离为 1 的所有节点,再搜索距离为 2 的...因此第一次到达终点时,经过的边数最少。

考点:无权图上 BFS 按经过的边数分层,第一次到达时步数最少。

易错:把这条结论推广到带权图——边权不全相同时必须用 Dijkstra,BFS 不再保证最短。

A3选择题

BFS 中,应该在什么时候标记节点为已访问?

A. 出队时   B. 入队时   C. 访问邻居时   D. 无所谓

查看参考答案

答案:B. 入队时

入队时立即标记可以防止同一节点被多次入队,避免队列爆炸和重复计算。出队时标记会导致同一节点被多个邻居重复入队。

考点:入队时立即标记可以保证每个节点只入队一次,避免重复入队导致队列膨胀。

易错:等出队时才标记,同一个节点会被多个邻居重复入队,队列暴涨、程序超内存或超时。

A4编程题

给定一个 n×m 的迷宫(0=空地,1=墙壁),从 (0,0) 到 (n-1,m-1) 的最短步数。如果不可达输出 -1。

查看参考答案
#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
int n,m,grid[105][105],dist[105][105];
int dx[]={-1,1,0,0},dy[]={0,0,-1,1};
int main(){
    cin>>n>>m;
    for(int i=0;i<n;i++)
        for(int j=0;j<m;j++) cin>>grid[i][j];
    memset(dist,-1,sizeof(dist));
    queue<pair<int,int>> q;
    q.push({0,0}); dist[0][0]=0;
    while(!q.empty()){
        auto [x,y]=q.front(); q.pop();
        for(int d=0;d<4;d++){
            int nx=x+dx[d],ny=y+dy[d];
            if(nx>=0&&nx<n&&ny>=0&&ny<m
                &&grid[nx][ny]==0&&dist[nx][ny]==-1){
                dist[nx][ny]=dist[x][y]+1;
                q.push({nx,ny});
            }
        }
    }
    cout<<dist[n-1][m-1]<<endl;
    return 0;
}

思路:用 dist 数组记录每个格子到起点的步数,从起点逐层 BFS 扩展,到达终点时 dist 即为最短步数。

易错:dist 没有初始化为 −1,未访问格子和距离为 0 的格子混在一起;或漏写终点不可达时输出 −1 的分支。

🟡 B组 · 提高(4题)

B1编程题

填涂颜色:给定 n×n 的 01 矩阵,将由 1 围成的封闭区域内的 0 全部改为 2。(洛谷 P1162 改编)

查看参考答案

思路:从边界上的 0 出发做 BFS,标记所有能到达边界的外部 0。剩下未被标记的 0 就是封闭区域内的,改为 2。

#include <iostream>
#include <queue>
using namespace std;
int n,g[35][35];
bool vis[35][35];
int dx[]={-1,1,0,0},dy[]={0,0,-1,1};
void bfs(int sx,int sy){
    queue<pair<int,int>> q;
    q.push({sx,sy}); vis[sx][sy]=true;
    while(!q.empty()){
        auto [x,y]=q.front(); q.pop();
        for(int d=0;d<4;d++){
            int nx=x+dx[d],ny=y+dy[d];
            if(nx>=0&&nx<n&&ny>=0&&ny<n
                &&g[nx][ny]==0&&!vis[nx][ny]){
                vis[nx][ny]=true;
                q.push({nx,ny});
            }
        }
    }
}
int main(){
    cin>>n;
    for(int i=0;i<n;i++)
        for(int j=0;j<n;j++) cin>>g[i][j];
    // 从边界的0出发BFS
    for(int i=0;i<n;i++){
        if(g[i][0]==0&&!vis[i][0]) bfs(i,0);
        if(g[i][n-1]==0&&!vis[i][n-1]) bfs(i,n-1);
    }
    for(int j=0;j<n;j++){
        if(g[0][j]==0&&!vis[0][j]) bfs(0,j);
        if(g[n-1][j]==0&&!vis[n-1][j]) bfs(n-1,j);
    }
    // 输出:未标记的0改为2
    for(int i=0;i<n;i++){
        for(int j=0;j<n;j++){
            if(g[i][j]==0&&!vis[i][j]) cout<2;
            else cout<<g[i][j];
            cout<" ";
        }
        cout<<endl;
    }
    return 0;
}

核心思想:反向思考 — 不是找封闭区域,而是标记所有"外部"的 0,剩下的就是"内部"的 0。

易错:从内部的 0 出发泛洪无法区分“封闭区域内”和“外部”——必须从边界(或加一圈虚拟外圈)开始标记。

B2编程题

字串变换:给定起始字符串 A 和目标字符串 B,以及若干替换规则(如 a→bc)。求最少替换次数使 A 变为 B。超过 10 步输出 NO。

查看参考答案

思路:把每个字符串看作图的一个节点,替换规则看作边。从 A 出发做 BFS,第一次到达 B 时的步数就是最少替换次数。

关键:用 set 或 map 记录已访问的字符串,防止重复搜索。超过 10 步直接输出 NO。

#include <iostream>
#include <queue>
#include <map>
#include <string>
using namespace std;
string ruleA[10],ruleB[10];
int cnt=0;
int bfs(string start,string target){
    queue<pair<string,int>> q;
    map<string,bool> vis;
    q.push({start,0}); vis[start]=true;
    while(!q.empty()){
        auto [s,step]=q.front(); q.pop();
        if(s==target) return step;
        if(step>=10) continue;
        for(int i=0;i<cnt;i++){
            size_t pos=0;
            while((pos=s.find(ruleA[i],pos))!=string::npos){
                string ns=s;
                ns.replace(pos,ruleA[i].length(),ruleB[i]);
                if(!vis[ns]){vis[ns]=true; q.push({ns,step+1});}
                pos++;
            }
        }
    }
    return -1;
}

注意:同一规则可能在字符串中出现多次,每次替换都产生一个新状态。

B3编程题

多源BFS:给定 n×m 矩阵,某些格子初始为 1(腐烂橘子),每分钟腐烂橘子会把上下左右相邻的 0(新鲜橘子)变成 1。求所有橘子都腐烂的最少分钟数。

查看参考答案

思路:多源BFS — 把所有初始腐烂的橘子同时入队,做 BFS,最后的层数就是答案。

#include <iostream>
#include <queue>
using namespace std;
int n,m,g[55][55];
int dx[]={-1,1,0,0},dy[]={0,0,-1,1};
int main(){
    cin>>n>>m;
    queue<pair<pair<int,int>,int>> q;
    int fresh=0;
    for(int i=0;i<n;i++)
        for(int j=0;j<m;j++){
            cin>>g[i][j];
            if(g[i][j]==1) q.push({{i,j},0});
            if(g[i][j]==0) fresh++;
        }
    int ans=0;
    while(!q.empty()){
        auto [pos,t]=q.front(); q.pop();
        auto [x,y]=pos; ans=t;
        for(int d=0;d<4;d++){
            int nx=x+dx[d],ny=y+dy[d];
            if(nx>=0&&nx<n&&ny>=0&&ny<m&&g[nx][ny]==0){
                g[nx][ny]=1; fresh--;
                q.push({{nx,ny},t+1});
            }
        }
    }
    cout<<(fresh==0?ans:-1)<<endl;
    return 0;
}

多源BFS:所有初始腐烂橘子同时入队(距离=0),一次BFS完成全部传播。

易错:对每个腐烂橘子单独做一次 BFS 再取最小值,而不是让所有火源同时入队,得到的分钟数会偏大甚至错误。

B4分析题

分析 BFS 和 DFS 在以下场景中的表现差异:在一个 100×100 的迷宫中找从左上角到右下角的最短路径。

查看参考答案

BFS 表现:

• 按层次扩展,第一次到达终点就是最短路径

• 不需要回溯,每个格子最多访问一次

• 时间复杂度 O(100×100) = O(10000),非常快

• 空间复杂度 O(10000)(队列+标记数组)

DFS 表现:

• 一条路走到黑,找到的第一条路径不一定是最短的

• 需要回溯探索所有路径才能找到最短的

• 最坏情况需要探索指数级路径数

• 结论:求最短路径,BFS 远优于 DFS

考点:求最短路径的关键在于“是否按层扩展”:BFS 第一次到达即最短,DFS 必须穷举所有路径。

易错:认为 DFS 只要每个格子访问一次就能得到最短路,忽略了它必须回溯枚举这一点。

🔴 C组 · 挑战(4题)

C1初赛真题

关于广度优先搜索(BFS),下列说法正确的是()

A. BFS 可以用来求有权图的最短路径
B. BFS 使用栈来实现
C. BFS 在无权图中第一次到达某节点的路径一定是最短的
D. BFS 的空间复杂度一定是 O(n²)

查看参考答案

答案:C

A错(有权图需 Dijkstra),B错(BFS 用队列),D错(空间 O(V))。C 正确:BFS 按层次扩展,先到即最短。

考点:BFS 适用于边权相同(或无权)的图,且需要 O(V) 级别的空间来存放队列和标记数组。

易错:认为 BFS 能求任意图的最短路,或把 BFS 的空间复杂度写成 O(1)。

C2综合编程

八数码问题:3×3 的棋盘上有 1~8 八个数字和一个空格(0)。每次可以把空格与相邻数字交换。给定初始状态和目标状态,求最少步数。

查看参考答案

思路:把棋盘的每种状态看作图的一个节点,交换操作看作边。从初始状态做 BFS,第一次到达目标状态的步数就是答案。

关键:用字符串表示棋盘状态(9个字符),用 set 或 unordered_set 记录已访问状态。

#include <iostream>
#include <queue>
#include <unordered_set>
#include <string>
using namespace std;
int dx[]={-1,1,0,0},dy[]={0,0,-1,1};
int bfs(string start, string target){
    if(start==target) return 0;
    queue<pair<string,int>> q;
    unordered_set#include <string> vis;
    q.push({start,0}); vis.insert(start);
    while(!q.empty()){
        auto [s,step]=q.front(); q.pop();
        int pos=s.find('0');
        int x=pos/3, y=pos%3;
        for(int d=0;d<4;d++){
            int nx=x+dx[d],ny=y+dy[d];
            if(nx>=0&&nx<3&&ny>=0&&ny<3){
                string ns=s;
                swap(ns[pos],ns[nx*3+ny]);
                if(ns==target) return step+1;
                if(vis.find(ns)==vis.end()){
                    vis.insert(ns);
                    q.push({ns,step+1});
                }
            }
        }
    }
    return -1;
}

状态总数:9! = 362880 种,BFS 可以在毫秒内完成。

易错:去重容器使用未去重或用数组下标表示状态时开得太小,同一状态反复入队,队列爆炸导致超内存。

C3综合编程

倒水问题:有两个容量分别为 A 和 B 的杯子(初始为空),可以进行三种操作:装满一杯、倒空一杯、从一杯倒入另一杯。求最少几步能在某杯中恰好得到 C 升水。

查看参考答案

思路:状态 = (杯1水量, 杯2水量),最多 6 种操作。从 (0,0) 做 BFS,找到任一水量 = C 的状态。

6种操作:①装满A ②装满B ③倒空A ④倒空B ⑤A倒B ⑥B倒A

#include <iostream>
#include <queue>
#include <set>
using namespace std;
int A,B,C;
int bfs(){
    queue<pair<pair<int,int>,int>> q;
    set<pair<int,int>> vis;
    q.push({{0,0},0}); vis.insert({0,0});
    while(!q.empty()){
        auto [st,step]=q.front(); q.pop();
        auto [a,b]=st;
        if(a==C||b==C) return step;
        // 6种操作
        pair<int,int> nexts[] = {
            {A,b},{a,B},{0,b},{a,0},
            {max(0,a+b-B),min(B,a+b)},
            {min(A,a+b),max(0,a+b-A)}
        };
        for(auto& ns : nexts)
            if(vis.find(ns)==vis.end()){
                vis.insert(ns);
                q.push({ns,step+1});
            }
    }
    return -1;
}

状态空间:(A+1)×(B+1) 种状态,BFS 保证找到最少步数。

易错:漏写状态去重(同一个 (A,B) 水量反复入队造成死循环);或倒水操作没用 min/max 限制,水量被算成负数。

C4综合编程

01 迷宫:给定 n×n 的 01 矩阵,从 (0,0) 到 (n-1,n-1)。你最多可以消除 k 面墙壁(把 1 变成 0)。求最短路径长度。

查看参考答案

思路:状态 = (x, y, 已消除墙壁数),用三维 dist[x][y][c] 记录。BFS 时:

• 走到空地(0):c 不变,dist+1

• 走到墙壁(1):如果 c < k,则 c+1,dist+1

#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
int n,k,g[55][55],dist[55][55][12];
int dx[]={-1,1,0,0},dy[]={0,0,-1,1};
struct State{int x,y,c;};
int bfs(){
    memset(dist,-1,sizeof(dist));
    queue<State> q;
    q.push({0,0,0}); dist[0][0][0]=0;
    while(!q.empty()){
        auto [x,y,c]=q.front(); q.pop();
        if(x==n-1&&y==n-1) return dist[x][y][c];
        for(int d=0;d<4;d++){
            int nx=x+dx[d],ny=y+dy[d];
            if(nx>=0&&nx<n&&ny>=0&&ny<n){
                int nc = c + g[nx][ny];
                if(nc<=k && dist[nx][ny][nc]==-1){
                    dist[nx][ny][nc]=dist[x][y][c]+1;
                    q.push({nx,ny,nc});
                }
            }
        }
    }
    return -1;
}

关键:状态增加一维"已消除墙壁数",使 BFS 能在带约束的网格中求最短路径。

易错:把“已消除墙壁数”只记在队列节点里,而没有写进 dist 的第三维,导致“用更少消除次数到达同一格”的状态被误判为已访问。

🏆 洛谷原题精选(20道)

以下题目均与 BFS 相关,按难度从易到难排列,建议在洛谷平台提交练习。

1P1162填涂颜色入门
2P1443马的遍历入门
3P114101迷宫入门
4P1332血色先锋队入门
5P1330封锁阳光大学入门
6P1551亲戚入门
7P1032字串变换普及
8P1126机器人搬重物普及
9P1379八数码难题普及
10P2296寻找道路普及
11P2895Meteor Shower普及
12P1434滑雪(BFS解法)普及
13P2730魔板 Magic Squares普及
14P1346电车普及
15P1285球队收入普及
16P1099树网的核提高
17P1341无序字母对提高
18P1156垃圾陷阱提高
19P1457城堡 Castle提高
20P2937Basketball Game提高

💡 洛谷刷题建议

入门(1-6题):掌握 BFS 基本模板,网格搜索、连通块、二分图判定

普及(7-15题):灵活应用 BFS — 状态搜索(八数码)、多源BFS、约束BFS

提高(16-20题):综合应用 — 树上BFS、状态压缩BFS、复杂约束搜索

建议:先独立完成,实在不会再看题解。每道题都要手写 BFS 模板,不要复制粘贴。