目录
[一、BFS 到底是什么?------ 从 "一层一层找答案" 说起](#一、BFS 到底是什么?—— 从 “一层一层找答案” 说起)
[1.1 BFS 的核心思想](#1.1 BFS 的核心思想)
[1.2 BFS 的基本框架](#1.2 BFS 的基本框架)
[二、BFS 经典例题实战 ------ 从基础到进阶](#二、BFS 经典例题实战 —— 从基础到进阶)
[2.1 基础款:马的遍历(洛谷 P1443)](#2.1 基础款:马的遍历(洛谷 P1443))
[2.2 进阶款:kotori 和迷宫(牛客网)](#2.2 进阶款:kotori 和迷宫(牛客网))
[2.3 优化款:Catch That Cow(洛谷 USACO07OPEN)](#2.3 优化款:Catch That Cow(洛谷 USACO07OPEN))
[2.4 高阶款:八数码难题(洛谷 P1379)](#2.4 高阶款:八数码难题(洛谷 P1379))
[三、BFS 的扩展玩法 ------ 多源 BFS、01 BFS](#三、BFS 的扩展玩法 —— 多源 BFS、01 BFS)
[3.1 多源 BFS:矩阵距离(牛客网)](#3.1 多源 BFS:矩阵距离(牛客网))
[3.2 01 BFS:小明的游戏(洛谷 P4554)](#3.2 01 BFS:小明的游戏(洛谷 P4554))
[四、BFS 的避坑指南 ------ 这些错误 90% 的人都会犯](#四、BFS 的避坑指南 —— 这些错误 90% 的人都会犯)
前言
在算法的世界里,搜索类算法是解决 "找路径、求最优" 问题的核心武器,而宽度优先搜索(BFS)更是其中的 "明星选手"------ 它凭借 "逐层扩展、最短路径" 的特性,成为处理边权为 1 的最短路问题的首选。今天,我们就从零开始,把 BFS 的底层逻辑、实现技巧、经典例题掰开揉碎讲清楚,让你不仅会写代码,更能理解 "为什么要这么写"。下面就让我们正式开始吧!
一、BFS 到底是什么?------ 从 "一层一层找答案" 说起
1.1 BFS 的核心思想
BFS(Breadth-First Search),直译是 "宽度优先搜索",也常被称作 "广度优先搜索"。如果说深度优先搜索(DFS)是 "一条路走到黑,走不通再回头",那 BFS 就是 "稳扎稳打,一层一层往外扩"。
想象一个场景:你站在迷宫的起点,想要找到出口,且要求走最少的步数。DFS 的做法是随便选一个方向冲到底,碰壁了就退回来换方向;而 BFS 的做法是,先把起点周围一步能到的位置都走一遍,再把这些位置周围两步能到的位置走一遍...... 直到找到出口。这种 "逐层探索" 的方式,天然保证了 "第一次遇到目标时,走的步数就是最少的"------ 这也是 BFS 最核心的优势:在边权为 1 的图 / 网格中,能直接求出起点到目标点的最短路径。
1.2 BFS 的基本框架
BFS 的实现离不开两个关键工具:队列 (用来存储待扩展的节点)和标记数组(用来记录节点是否被访问过,避免重复搜索)。
我们先给出 BFS 的通用模板(以网格类问题为例),后续所有例题都会基于这个模板变形:
cpp
#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
// 定义方向数组(上下左右)
int dx[] = {0, 0, 1, -1};
int dy[] = {1, -1, 0, 0};
// 标记数组:记录是否访问过,也可记录最短距离
int dist[100][100];
// 网格大小
int n, m;
void bfs(int start_x, int start_y) {
// 初始化:距离数组置为-1(表示未访问)
memset(dist, -1, sizeof dist);
// 队列存储坐标对
queue<pair<int, int>> q;
// 起点入队,距离置为0
q.push({start_x, start_y});
dist[start_x][start_y] = 0;
// 队列不为空时循环
while (!q.empty()) {
// 取出队头节点
auto t = q.front();
q.pop();
int x = t.first, y = t.second;
// 扩展四个方向
for (int i = 0; i < 4; i++) {
int nx = x + dx[i];
int ny = y + dy[i];
// 检查边界 + 未访问
if (nx >= 1 && nx <= n && ny >= 1 && ny <= m && dist[nx][ny] == -1) {
// 更新距离:当前距离+1
dist[nx][ny] = dist[x][y] + 1;
// 新节点入队
q.push({nx, ny});
// 找到目标可提前退出(可选)
if (nx == 目标x && ny == 目标y) return;
}
}
}
}
这个模板看似简单,但藏着几个关键细节:
- 队列的作用 :保证 "先进先出",符合 BFS "逐层扩展" 的逻辑;
- 距离数组 dist:既标记是否访问,又记录最短距离,一举两得;
- 方向数组:把 "上下左右" 的移动转化为循环,简化代码;
- 提前退出:如果目标明确,找到后可直接 return,减少不必要的计算。
二、BFS 经典例题实战 ------ 从基础到进阶
光说不练假把式,接下来我们结合 4 道经典例题,一步步拆解 BFS 的应用技巧,从 "马的遍历" 到 "八数码难题",难度由浅入深,带你吃透 BFS 的精髓。
2.1 基础款:马的遍历(洛谷 P1443)
题目链接: https://www.luogu.com.cn/problem/P1443

题目描述
给定一个 n×m 的棋盘,马初始在 (x,y) 位置,求马到达棋盘上任意点的最少步数(不能到达则输出 - 1)。
核心分析
这是 BFS 的入门题,核心考点是**"多方向扩展"**------ 马走 "日",有 8 个移动方向,只需把方向数组改一下,其余逻辑和通用模板一致。
完整代码
cpp
#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
typedef pair<int, int> PII;
const int N = 410;
int n, m, x, y;
int dist[N][N];
// 马的8个移动方向(走日)
int dx[] = {1, 2, 2, 1, -1, -2, -2, -1};
int dy[] = {2, 1, -1, -2, -2, -1, 1, 2};
void bfs() {
memset(dist, -1, sizeof dist);
queue<PII> q;
q.push({x, y});
dist[x][y] = 0;
while (q.size()) {
auto t = q.front();
q.pop();
int i = t.first, j = t.second;
for (int k = 0; k < 8; k++) {
int nx = i + dx[k], ny = j + dy[k];
// 边界检查 + 未访问
if (nx >= 1 && nx <= n && ny >= 1 && ny <= m && dist[nx][ny] == -1) {
dist[nx][ny] = dist[i][j] + 1;
q.push({nx, ny});
}
}
}
}
int main() {
cin >> n >> m >> x >> y;
bfs();
// 输出结果矩阵
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cout << dist[i][j] << " ";
}
cout << endl;
}
return 0;
}
关键细节
- 马的方向数组是本题的唯一难点,记住 "走日" 的 8 个组合即可;
- 距离数组初始化为 - 1,起点置 0,扩展时每一步距离 + 1,天然满足 "最短步数";
- 无需提前退出,因为要计算所有点的距离,需遍历完整个队列。
2.2 进阶款:kotori 和迷宫(牛客网)
题目链接: https://ac.nowcoder.com/acm/problem/50041

题目描述
n×m 的迷宫,最外层是岩浆(无法走),有 k 个出口('e'),起点是 'k',道路是 '.',墙壁是 '*'。求能到达的出口数量,以及最近出口的步数(无法到达输出 - 1)。
核心分析
这道题在基础 BFS 上增加了 "多目标判断":需要遍历所有出口,统计可达数量和最短距离。核心思路是:
- 先用 BFS 求出起点到所有点的最短距离;
- 遍历整个迷宫,筛选出标记为 'e' 且距离不为 - 1 的位置,统计数量和最小值。
完整代码
cpp
#include <iostream>
#include <queue>
#include <cstring>
#include <climits>
using namespace std;
typedef pair<int, int> PII;
const int N = 35;
int n, m, start_x, start_y;
char a[N][N];
int dist[N][N];
int dx[] = {0, 0, 1, -1};
int dy[] = {1, -1, 0, 0};
void bfs() {
memset(dist, -1, sizeof dist);
queue<PII> q;
q.push({start_x, start_y});
dist[start_x][start_y] = 0;
while (q.size()) {
auto t = q.front();
q.pop();
int i = t.first, j = t.second;
for (int k = 0; k < 4; k++) {
int nx = i + dx[k], ny = j + dy[k];
// 边界 + 不是墙壁 + 未访问
if (nx >= 1 && nx <= n && ny >= 1 && ny <= m && a[nx][ny] != '*' && dist[nx][ny] == -1) {
dist[nx][ny] = dist[i][j] + 1;
q.push({nx, ny});
}
}
}
}
int main() {
cin >> n >> m;
// 读入迷宫,定位起点
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> a[i][j];
if (a[i][j] == 'k') {
start_x = i;
start_y = j;
}
}
}
bfs();
// 统计出口数量和最短距离
int cnt = 0;
int min_step = INT_MAX;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (a[i][j] == 'e' && dist[i][j] != -1) {
cnt++;
min_step = min(min_step, dist[i][j]);
}
}
}
if (cnt == 0) {
cout << -1 << endl;
} else {
cout << cnt << " " << min_step << endl;
}
return 0;
}
关键细节
- 迷宫类问题的核心是**"合法路径判断":本题中需排除墙壁('*')和岩浆(题目说最外层是岩浆,只需限制nx/ny**在 1~n/1~m 即可);
- 最短距离初始化用 INT_MAX(需包含<climits>),避免初始值干扰最小值计算;
- 统计阶段要同时满足 "是出口" 和 "可达(距离≠-1)" 两个条件。
2.3 优化款:Catch That Cow(洛谷 USACO07OPEN)
题目链接: https://www.luogu.com.cn/problem/P1588

题目描述
FJ 和牛在一维直线上,初始位置 x 和 y(牛不动)。FJ 的移动方式:前进一步(x+1)、后退一步(x-1)、瞬移(x×2),求追上牛的最少步数。
核心分析
这是一维 BFS 的经典题,看似简单,但不剪枝会超时!因为 x 可能达到 1e5,盲目扩展会导致队列过大。核心优化点(剪枝策略):
- 后退到负数:剪掉(退到负数再回来,步数一定更多);
- 前进超过 y:剪掉(已经在牛右边,再前进只会离得更远);
- 瞬移超过 2*y:剪掉(若 y 是偶数,瞬移到超过 y 不如先减到 y/2 再瞬移;若 y 是奇数,超过 y+1 也无意义)。
完整代码
cpp
#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
const int N = 1e5 + 10;
int dist[N];
int x, y;
void bfs() {
memset(dist, -1, sizeof dist);
queue<int> q;
q.push(x);
dist[x] = 0;
while (q.size()) {
int t = q.front();
q.pop();
// 找到目标,提前退出
if (t == y) return;
// 扩展三种移动方式
// 1. 前进一步
if (t + 1 <= N - 10 && dist[t + 1] == -1) {
dist[t + 1] = dist[t] + 1;
q.push(t + 1);
}
// 2. 后退一步(不能为负)
if (t - 1 >= 0 && dist[t - 1] == -1) {
dist[t - 1] = dist[t] + 1;
q.push(t - 1);
}
// 3. 瞬移(不超过2*y,避免无效扩展)
if (t * 2 <= 2 * y && dist[t * 2] == -1) {
dist[t * 2] = dist[t] + 1;
q.push(t * 2);
}
}
}
int main() {
int T;
cin >> T;
while (T--) {
cin >> x >> y;
bfs();
cout << dist[y] << endl;
}
return 0;
}
关键细节
- 一维 BFS 的队列存储的是 "位置值",而非坐标对,简化了数据结构;
- 剪枝是本题的核心:瞬移的**剪枝条件(t2 <= 2y)**能大幅减少队列大小,避免超时;
- 多组数据时,每次 BFS 前要重置 dist 数组(memset),否则会继承上一组的结果。
2.4 高阶款:八数码难题(洛谷 P1379)
题目链接: https://www.luogu.com.cn/problem/P1379

题目描述
3×3 棋盘上有 8 个棋子(1~8)和一个空格(0),空格周围的棋子可移动到空格。给定初始状态,求到目标状态(123804765)的最少步数。
核心分析
这是 BFS 的经典难题,核心难点是**"状态表示"** 和**"状态转换"**:
- 状态表示:把 3×3 棋盘转化为字符串(如初始状态 283104765 对应字符串 "283104765"),方便存储和哈希;
- 状态转换:找到空格的位置(pos),计算其在 3×3 棋盘中的二维坐标(x=pos/3,y=pos%3),然后交换空格与上下左右棋子的位置,生成新状态;
- 距离记录:用 **unordered_map<string, int>**存储每个状态的最短距离,避免重复访问。

完整代码
cpp
#include <iostream>
#include <queue>
#include <unordered_map>
using namespace std;
string aim = "123804765";
unordered_map<string, int> dist;
// 方向数组(上下左右)
int dx[] = {0, 0, 1, -1};
int dy[] = {1, -1, 0, 0};
void bfs(string start) {
queue<string> q;
q.push(start);
dist[start] = 0;
while (q.size()) {
string t = q.front();
q.pop();
// 找到目标,提前退出
if (t == aim) return;
// 找到空格的位置
int pos = t.find('0');
int x = pos / 3, y = pos % 3;
// 扩展四个方向
for (int i = 0; i < 4; i++) {
int nx = x + dx[i];
int ny = y + dy[i];
// 边界检查
if (nx >= 0 && nx < 3 && ny >= 0 && ny < 3) {
// 计算新状态的空格位置
int new_pos = nx * 3 + ny;
// 交换空格与相邻棋子
string next = t;
swap(next[pos], next[new_pos]);
// 未访问过则更新距离
if (!dist.count(next)) {
dist[next] = dist[t] + 1;
q.push(next);
}
}
}
}
}
int main() {
string s;
cin >> s;
bfs(s);
cout << dist[aim] << endl;
return 0;
}
关键细节
- 状态转换的核心是 "一维下标与二维坐标的转换":pos = x*3 + y,x = pos/3,y = pos%3(这个技巧在网格类问题中高频使用);
- unordered_map 的 count 方法:判断状态是否已访问,避免重复入队;
- 字符串的 find方法:快速定位空格('0')的位置,简化代码。
三、BFS 的扩展玩法 ------ 多源 BFS、01 BFS
除了基础 BFS,还有两种高频变种:多源 BFS 和01 BFS,它们是解决复杂问题的 "杀手锏"。
3.1 多源 BFS:矩阵距离(牛客网)
题目链接: https://ac.nowcoder.com/acm/problem/51024

核心思想
当问题有多个起点时,把所有起点一次性加入队列,视为 "超级源点",然后按普通 BFS 扩展 ------ 本质是 "把多个单源 BFS 合并为一次遍历",大幅降低时间复杂度。
典型场景
求矩阵中每个 0 到最近的 1 的曼哈顿距离(曼哈顿距离:|x1-x2| + |y1-y2|)。
核心代码片段
cpp
void bfs() {
memset(dist, -1, sizeof dist);
queue<PII> q;
// 所有1的位置入队(多源起点)
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (a[i][j] == '1') {
q.push({i, j});
dist[i][j] = 0;
}
}
}
// 普通BFS扩展
while (q.size()) {
auto t = q.front();
q.pop();
int x = t.first, y = t.second;
for (int i = 0; i < 4; i++) {
int nx = x + dx[i], ny = y + dy[i];
if (nx >= 1 && nx <= n && ny >= 1 && ny <= m && dist[nx][ny] == -1) {
dist[nx][ny] = dist[x][y] + 1;
q.push({nx, ny});
}
}
}
}
3.2 01 BFS:小明的游戏(洛谷 P4554)
题目链接: https://www.luogu.com.cn/problem/P4554

核心思想
当边权只有 0 和 1 时,用**双端队列(deque)**替代普通队列:
- 边权为 0:新节点加入队头(优先扩展,保证距离最短);
- 边权为 1:新节点加入队尾(常规扩展)。这种方式能在 O (nm) 时间内求出最短路径,比 Dijkstra 算法更高效。
典型场景
棋盘移动,走相同类型格子费用 0,不同类型费用 1,求最小费用。
核心代码片段
cpp
int bfs() {
memset(dist, -1, sizeof dist);
deque<PII> q;
q.push_back({x1, y1});
dist[x1][y1] = 0;
while (q.size()) {
auto t = q.front();
q.pop_front();
int a = t.first, b = t.second;
if (a == x2 && b == y2) return dist[a][b];
for (int i = 0; i < 4; i++) {
int nx = a + dx[i], ny = b + dy[i];
if (nx >= 1 && nx <= n && ny >= 1 && ny <= m && dist[nx][ny] == -1) {
if (g[a][b] == g[nx][ny]) {
// 边权0,加入队头
dist[nx][ny] = dist[a][b];
q.push_front({nx, ny});
} else {
// 边权1,加入队尾
dist[nx][ny] = dist[a][b] + 1;
q.push_back({nx, ny});
}
}
}
}
return -1;
}
四、BFS 的避坑指南 ------ 这些错误 90% 的人都会犯
- 忘记标记访问状态:导致节点重复入队,队列无限膨胀,超时或内存溢出;
- 边界判断错误:比如把 "nx <= n" 写成 "nx < n",漏掉最后一行 / 列,导致结果错误;
- 多组数据未重置数组:dist、st 等数组继承上一组结果,逻辑混乱;
- 方向数组写错:比如把马的方向数组写漏,导致扩展不完整;
- 状态表示不当:如八数码用二维数组存储状态,无法哈希,导致重复访问。
总结
BFS 看似简单,但想要用得 "炉火纯青",需要吃透底层逻辑,更要多练、多总结。希望这篇文章能帮你打通 BFS 的任督二脉,下次遇到搜索问题,能第一时间想到 "用 BFS 是不是更优?"------ 这才是真正掌握了这一算法的精髓。
最后,留一个小问题:八数码问题中,如果目标状态不固定,如何优化状态存储?欢迎在评论区交流~