1 广度优先搜索(BFS)详解

广度优先搜索(Breadth-First Search,简称 BFS)是图论中最基础的遍历算法之一,它的核心思想是 "逐层扩散":从起点出发,先访问距离最近的所有节点,再访问距离次近的节点,以此类推,直到遍历完所有可达节点。
一、BFS 的核心特点
- 按 "距离" 遍历:先访问起点(距离 0),再访问所有直接相邻的节点(距离 1),再访问距离 2 的节点......
- 队列辅助实现:使用队列(先进先出,FIFO)存储待访问节点,确保 "先入队的节点先被处理"。
- 避免重复访问:需要一个标记数组记录已访问的节点,防止循环访问。
二、BFS 的适用场景
- 最短路径问题(在无权图中,BFS 找到的路径是最短路径)。
- 层序遍历(如二叉树的层序遍历本质是 BFS)。
- 连通性判断(判断两个节点是否可达)。
三、BFS 的步骤(以图为例)
假设我们有一个图,用邻接表存储(邻接表是图的常用存储方式,每个节点对应一个列表,记录其直接相邻的节点)。BFS 的步骤如下:
- 初始化一个队列,将起点加入队列,并标记为 "已访问"。
- 当队列不为空时,取出队首节点,访问该节点。
- 遍历该节点的所有未访问的相邻节点,将它们加入队列,并标记为 "已访问"。
- 重复步骤 2 和 3,直到队列为空(所有可达节点均被访问)。
四、代码实现(C++)
下面用一个具体例子说明:假设有一个无向图,节点编号为 0~4,边的连接如下:
bash
0 connected to 1, 2
1 connected to 0, 3, 4
2 connected to 0
3 connected to 1
4 connected to 1
用邻接表表示为:
bash
adj[0] = {1, 2}
adj[1] = {0, 3, 4}
adj[2] = {0}
adj[3] = {1}
adj[4] = {1}
我们从节点 0 开始 BFS,预期遍历顺序为:0 → 1 → 2 → 3 → 4。
代码实现:
cpp
#include <iostream>
#include <vector>
#include <queue> // 队列头文件
using namespace std;
// BFS函数:参数为邻接表和起点
void bfs(const vector<vector<int>>& adj, int start) {
int n = adj.size(); // 节点总数
vector<bool> visited(n, false); // 标记是否访问过
queue<int> q; // 存储待访问节点的队列
// 初始化:起点入队并标记
q.push(start);
visited[start] = true;
while (!q.empty()) { // 队列不为空时循环
int current = q.front(); // 取出队首节点
q.pop();
cout << current << " "; // 访问当前节点(此处为打印)
// 遍历当前节点的所有相邻节点
for (int neighbor : adj[current]) {
if (!visited[neighbor]) { // 若未访问过
visited[neighbor] = true; // 标记为已访问
q.push(neighbor); // 加入队列
}
}
}
}
int main() {
// 构建邻接表
vector<vector<int>> adj = {
{1, 2}, // 0的邻居:1,2
{0, 3, 4}, // 1的邻居:0,3,4
{0}, // 2的邻居:0
{1}, // 3的邻居:1
{1} // 4的邻居:1
};
cout << "BFS遍历结果:";
bfs(adj, 0); // 从节点0开始BFS
return 0;
}
输出结果:
bash
BFS遍历结果:0 1 2 3 4
五、代码解析
- 邻接表 :用
vector<vector<int>> adj存储图,adj[i]表示节点i的所有相邻节点。 - 队列 :
queue<int> q用于存储 "待访问的节点",确保按顺序处理。 - 访问标记 :
vector<bool> visited防止重复访问(避免死循环,例如无向图中 A-B-A 的循环)。 - 遍历逻辑:每次队首节点→访问→将未访问的邻居入队,直到层扩散。
六、BFS 的时间复杂度
- 设图的节点数为
V,边数为E。 - 每个节点入队、出队各一次,时间
O(V)。 - 每条边最多被遍历一次(无向图每条边被两个节点各遍历一次),时间
O(E)。 - 总时间复杂度:
O(V + E)。
通过这个例子,你应该能理解 BFS 的核心逻辑了。BFS 的关键是 "队列 + 标记",记住 "逐层扩散" 的特点,就能灵活应用在各种场景中。如果有具体问题(如最短路径),可以进一步扩展这个框架。
2 广搜BFS的ACM 模式(标准输入输出)详解
一、ACM 模式需求说明
我们需要实现一个程序:
- 从标准输入读取图的信息(节点数、边数、边的连接关系)
- 从指定起点开始进行 BFS 遍历
- 将遍历结果输出到标准输出
二、输入输出格式
输入格式:
- 第一行:两个整数
n和m,分别表示节点数(节点编号 0~n-1)和边数 - 接下来
m行:每行两个整数u和v,表示节点u和v之间有一条无向边(双向连接) - 最后一行:一个整数
start,表示 BFS 的起点
输出格式:
- 一行:BFS 遍历的节点顺序,用空格分隔
三、完整代码实现(ACM 模式)
cpp
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
// BFS核心函数:输入邻接表和起点,输出遍历顺序
void bfs(const vector<vector<int>>& adj, int start) {
int n = adj.size(); // 节点总数
vector<bool> visited(n, false); // 标记数组:记录节点是否已访问
queue<int> q; // 队列:存储待访问的节点
// 1. 初始化:起点入队并标记为已访问
q.push(start);
visited[start] = true;
// 2. 队列非空时,循环处理节点
while (!q.empty()) {
// 2.1 取出队首节点
int current = q.front();
q.pop();
// 2.2 访问当前节点(这里直接输出)
cout << current << " ";
// 2.3 遍历当前节点的所有邻居,未访问的入队
for (int neighbor : adj[current]) {
if (!visited[neighbor]) { // 若邻居未访问
visited[neighbor] = true; // 标记为已访问
q.push(neighbor); // 加入队列
}
}
}
}
int main() {
// 步骤1:读取输入
int n, m;
cin >> n >> m; // 节点数和边数
// 步骤2:构建邻接表(初始化空列表)
vector<vector<int>> adj(n);
// 步骤3:读取每条边,填充邻接表(无向图双向添加)
for (int i = 0; i < m; i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v); // u的邻居添加v
adj[v].push_back(u); // v的邻居添加u(无向图关键)
}
// 步骤4:读取起点
int start;
cin >> start;
// 步骤5:执行BFS并输出结果
bfs(adj, start);
return 0;
}
四、输入输出流程详解
以文中的例子为例,输入输出过程如下:
输入示例:
bash
5 5 // 5个节点,5条边
0 1 // 边:0-1
0 2 // 边:0-2
1 3 // 边:1-3
1 4 // 边:1-4
2 0 // 边:2-0(无向图重复,也可省略)
0 // 起点为0
处理流程:
- 读取节点数和边数 :
n=5,m=5,表示有 0~4 共 5 个节点。 - 构建邻接表 :
- 初始为
adj = [[], [], [], [], []] - 读取边
0-1:adj[0]加入 1,adj[1]加入 0 →adj[0] = [1],adj[1] = [0] - 读取边
0-2:adj[0]加入 2,adj[2]加入 0 →adj[0] = [1,2],adj[2] = [0] - 读取边
1-3:adj[1]加入 3,adj[3]加入 1 →adj[1] = [0,3],adj[3] = [1] - 读取边
1-4:adj[1]加入 4,adj[4]加入 1 →adj[1] = [0,3,4],adj[4] = [1] - 最终邻接表与文中一致。
- 初始为
- 执行 BFS :从起点 0 开始,按流程输出
0 1 2 3 4。
五、手搓代码的关键细节(必掌握)
-
邻接表的构建:
- 无向图必须双向添加(
u→v和v→u都要加),否则会漏边。 - 节点编号从 0 开始更方便(避免数组越界)。
- 无向图必须双向添加(
-
队列的操作:
- 先
push入队,再标记visited(防止重复入队)。 - 每次处理节点时,先
front()取队首,再pop()移除。
- 先
-
访问标记的时机:
- 必须在入队时标记,而不是出队时!否则可能导致同一个节点多次入队(例如节点 1 可能被 0 和 2 同时发现,若入队时不标记,会被加入队列两次)。
-
边界情况处理:
- 节点数为 0 或 1 时的逻辑(代码已兼容,无需额外处理)。
- 图不连通的情况(BFS 只会遍历起点可达的节点,符合预期)。
六、练习建议
- 用上述代码运行输入示例,观察输出是否正确。
- 修改起点(如从 1 开始),手动推导遍历顺序,再用代码验证。
- 尝试删除一条边(如删除 0-2),观察遍历结果的变化。
- 自己画一个图(如 3 个节点的三角形),写出输入并运行代码。
3 直观的图 + 对应的输入 + BFS 输出
我来画一个 直观的图 + 对应的输入 + BFS 输出,帮你看清三者的对应关系,你跟着走一遍就能彻底明白!
一、先画一个简单的图(新手友好)
我们画一个 4 个节点、4 条边的无向图,节点编号 0~3,连接关系如下:
- 节点 0 连 1、3
- 节点 1 连 0、2
- 节点 2 连 1
- 节点 3 连 0
图形长这样(用文字直观表示):
bash
2
/
1 --- 0 --- 3
- 核心:起点选 0,按 BFS "逐层扩散" 规则,遍历顺序应该是「0 → 1 → 3 → 2」(先访问距离 0 的 0,再距离 1 的 1、3,最后距离 2 的 2)。
二、对应的 ACM 模式输入
按照之前的输入格式,这个图的输入是:
bash
4 4 // 4个节点(0~3),4条边
0 1 // 边:0-1
0 3 // 边:0-3
1 2 // 边:1-2
3 0 // 边:3-0(无向图,可省略,不影响结果)
0 // BFS起点为0
三、代码运行后的输出
把上面的输入代入之前的 ACM 模式代码,输出结果是:
0 1 3 2
四、"图 → 输入 → 输出" 的对应逻辑(关键!)
-
图 → 输入:输入的本质是 "把图的连接关系用文字描述出来"。
- 图里有一条边,输入就写一行
u v。 - 无向图的边是双向的,所以
0 1和1 0是一回事,输入时写其中一行就行(代码会自动双向添加到邻接表)。
- 图里有一条边,输入就写一行
-
输入 → 邻接表 → 输出:
- 输入被代码读取后,会构建出邻接表:
adj[0]={1,3}、adj[1]={0,2}、adj[2]={1}、adj[3]={0}。 - BFS 按 "队列 + 标记" 规则遍历邻接表,最终输出遍历顺序。
- 输入被代码读取后,会构建出邻接表:
-
核心对应:图的 "连接关系" 决定输入,输入决定邻接表,邻接表决定 BFS 输出 ------ 三者是严格对应的,只要输入没错,输出就和 "图的逐层扩散顺序" 完全一致。
五、再试一个 "不连通图"(帮你巩固)
如果图是这样(节点 4 孤立):
2
/
1 --- 0 --- 3 4(孤立节点)
-
输入(5 个节点,4 条边):
5 4 0 1 0 3 1 2 3 0 0 -
输出:
0 1 3 2(孤立节点 4 因为和起点 0 不连通,不会被遍历到,这是正常现象)。