算法笔记 13 BFS | 图

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

9.3 图的遍历 - Hello 算法

广度优先搜索(Breadth-First Search,简称 BFS)是图论中最基础的遍历算法之一,它的核心思想是 "逐层扩散":从起点出发,先访问距离最近的所有节点,再访问距离次近的节点,以此类推,直到遍历完所有可达节点。

一、BFS 的核心特点
  1. 按 "距离" 遍历:先访问起点(距离 0),再访问所有直接相邻的节点(距离 1),再访问距离 2 的节点......
  2. 队列辅助实现:使用队列(先进先出,FIFO)存储待访问节点,确保 "先入队的节点先被处理"。
  3. 避免重复访问:需要一个标记数组记录已访问的节点,防止循环访问。
二、BFS 的适用场景
  • 最短路径问题(在无权图中,BFS 找到的路径是最短路径)。
  • 层序遍历(如二叉树的层序遍历本质是 BFS)。
  • 连通性判断(判断两个节点是否可达)。
三、BFS 的步骤(以图为例)

假设我们有一个图,用邻接表存储(邻接表是图的常用存储方式,每个节点对应一个列表,记录其直接相邻的节点)。BFS 的步骤如下:

  1. 初始化一个队列,将起点加入队列,并标记为 "已访问"。
  2. 当队列不为空时,取出队首节点,访问该节点。
  3. 遍历该节点的所有未访问的相邻节点,将它们加入队列,并标记为 "已访问"。
  4. 重复步骤 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 
五、代码解析
  1. 邻接表 :用vector<vector<int>> adj存储图,adj[i]表示节点i的所有相邻节点。
  2. 队列queue<int> q用于存储 "待访问的节点",确保按顺序处理。
  3. 访问标记vector<bool> visited防止重复访问(避免死循环,例如无向图中 A-B-A 的循环)。
  4. 遍历逻辑:每次队首节点→访问→将未访问的邻居入队,直到层扩散。
六、BFS 的时间复杂度
  • 设图的节点数为V,边数为E
  • 每个节点入队、出队各一次,时间O(V)
  • 每条边最多被遍历一次(无向图每条边被两个节点各遍历一次),时间O(E)
  • 总时间复杂度:O(V + E)

通过这个例子,你应该能理解 BFS 的核心逻辑了。BFS 的关键是 "队列 + 标记",记住 "逐层扩散" 的特点,就能灵活应用在各种场景中。如果有具体问题(如最短路径),可以进一步扩展这个框架。

2 广搜BFS的ACM 模式(标准输入输出)详解

一、ACM 模式需求说明

我们需要实现一个程序:

  1. 从标准输入读取图的信息(节点数、边数、边的连接关系)
  2. 从指定起点开始进行 BFS 遍历
  3. 将遍历结果输出到标准输出

二、输入输出格式

输入格式:
  • 第一行:两个整数 nm,分别表示节点数(节点编号 0~n-1)和边数
  • 接下来 m 行:每行两个整数 uv,表示节点 uv 之间有一条无向边(双向连接)
  • 最后一行:一个整数 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
处理流程:
  1. 读取节点数和边数n=5m=5,表示有 0~4 共 5 个节点。
  2. 构建邻接表
    • 初始为 adj = [[], [], [], [], []]
    • 读取边 0-1adj[0] 加入 1,adj[1] 加入 0 → adj[0] = [1]adj[1] = [0]
    • 读取边 0-2adj[0] 加入 2,adj[2] 加入 0 → adj[0] = [1,2]adj[2] = [0]
    • 读取边 1-3adj[1] 加入 3,adj[3] 加入 1 → adj[1] = [0,3]adj[3] = [1]
    • 读取边 1-4adj[1] 加入 4,adj[4] 加入 1 → adj[1] = [0,3,4]adj[4] = [1]
    • 最终邻接表与文中一致。
  3. 执行 BFS :从起点 0 开始,按流程输出 0 1 2 3 4

五、手搓代码的关键细节(必掌握)

  1. 邻接表的构建

    • 无向图必须双向添加(u→vv→u 都要加),否则会漏边。
    • 节点编号从 0 开始更方便(避免数组越界)。
  2. 队列的操作

    • push 入队,再标记 visited(防止重复入队)。
    • 每次处理节点时,先 front() 取队首,再 pop() 移除。
  3. 访问标记的时机

    • 必须在入队时标记,而不是出队时!否则可能导致同一个节点多次入队(例如节点 1 可能被 0 和 2 同时发现,若入队时不标记,会被加入队列两次)。
  4. 边界情况处理

    • 节点数为 0 或 1 时的逻辑(代码已兼容,无需额外处理)。
    • 图不连通的情况(BFS 只会遍历起点可达的节点,符合预期)。

六、练习建议

  1. 用上述代码运行输入示例,观察输出是否正确。
  2. 修改起点(如从 1 开始),手动推导遍历顺序,再用代码验证。
  3. 尝试删除一条边(如删除 0-2),观察遍历结果的变化。
  4. 自己画一个图(如 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

四、"图 → 输入 → 输出" 的对应逻辑(关键!)

  1. 图 → 输入:输入的本质是 "把图的连接关系用文字描述出来"。

    • 图里有一条边,输入就写一行 u v
    • 无向图的边是双向的,所以 0 11 0 是一回事,输入时写其中一行就行(代码会自动双向添加到邻接表)。
  2. 输入 → 邻接表 → 输出

    • 输入被代码读取后,会构建出邻接表:adj[0]={1,3}adj[1]={0,2}adj[2]={1}adj[3]={0}
    • BFS 按 "队列 + 标记" 规则遍历邻接表,最终输出遍历顺序。
  3. 核心对应:图的 "连接关系" 决定输入,输入决定邻接表,邻接表决定 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 不连通,不会被遍历到,这是正常现象)。

相关推荐
普通网友2 小时前
嵌入式C++安全编码
开发语言·c++·算法
烤麻辣烫2 小时前
黑马程序员苍穹外卖(新手) DAY3
java·开发语言·spring boot·学习·intellij-idea
驯狼小羊羔2 小时前
学习随笔-hooks和mixins
前端·javascript·vue.js·学习·hooks·mixins
组合缺一2 小时前
Solon AI 开发学习 - 1导引
java·人工智能·学习·ai·openai·solon
普通网友2 小时前
分布式锁服务实现
开发语言·c++·算法
普通网友2 小时前
移动语义在容器中的应用
开发语言·c++·算法
Bony-2 小时前
Articulation Point(割点)算法详解
算法·深度优先
热心市民小刘05052 小时前
11.18二叉树中序遍历(递归)
数据结构·算法
brave and determined2 小时前
可编程逻辑器件学习(day24):异构计算:突破算力瓶颈的未来之路
人工智能·嵌入式硬件·深度学习·学习·算法·fpga·asic