广度优先搜索 (BFS)

在这里插入图片描述


如果觉得本文对您有所帮助,点个赞和关注吧,谢谢!!!你的支持就是我持续更新的最大动力


引言

广度优先搜索(BFS)是图论算法中一块不可或缺的基石。与深度优先搜索(DFS)的“一路走到黑”策略不同,BFS 展现的是一种更为稳健、逐层推进的探索方式。其核心思想是从一个起始节点出发,系统性地探索图中的所有节点,确保在探索任何深度为 d 的节点之前,所有深度小于 d 的节点都已经被探索完毕。

这种“涟漪式”的扩散特性,使得 BFS 成为解决特定问题的天然选择,尤其是在涉及最短路径分层处理的场景中。

一、 BFS 核心原理与 C++ 实现范式

核心思想与数据结构选择

BFS 的本质是**按层级(或距离)**进行遍历。为了实现这一点,我们需要一个能够维持节点访问顺序的数据结构。

  1. 先进先出 (FIFO) 原则:当我们访问一个节点 u 时,会将其所有未访问的邻居节点加入待处理列表。我们希望先处理完 u 同层的所有节点,再去处理它们的邻居(下一层)。这精确地匹配了队列 (Queue) 的特性。
  2. 防止重复与死循环:在图中,节点之间可能存在环路。如果不加标记,搜索可能会在环路中无限循环。因此,我们需要一个辅助数据结构来记录已经访问过或已经加入队列的节点,通常称为 visited 集合。

标准实现模板详解

一个健壮且通用的 BFS 实现模板是掌握其应用的基础。

#include <iostream>
#include <vector>
#include <queue>
#include <unordered_set>

// 假设图用邻接表表示
// Graph[i] 存储了节点 i 的所有邻居节点
using Graph = std::vector<std::vector<int>>;

/**
 * @brief 对图进行广度优先搜索的通用模板
 * @param graph 表示图的邻接表,graph[i] 包含节点 i 的所有邻居。
 * @param start_node 搜索的起始节点。
 */
void bfs_template(const Graph& graph, int start_node) {
    // 1. 初始化
    // 队列:存储待访问的节点。节点按层级顺序入队和出队。
    std::queue<int> q;
    // 访问标记:使用哈希集合 O(1) 的平均查找效率,防止重复访问。
    // 对于节点是 0 到 n-1 的连续整数,`std::vector<bool>` 性能更优。
    std::unordered_set<int> visited;

    // 2. 起始状态
    // 将起始节点推入队列,作为第 0 层的唯一节点。
    q.push(start_node);
    // 立即标记为已访问,防止其他节点将其再次入队。
    visited.insert(start_node);

    // 3. 循环处理队列中的节点,直到队列为空
    while (!q.empty()) {
        // 3.1. 取出队首元素进行处理。这个节点是当前层中最早被发现的。
        int current_node = q.front();
        q.pop();

        // ---------------- [核心处理逻辑] ----------------
        // 在这里可以对 current_node 进行任何操作,例如打印、检查等。
        std::cout << current_node << " ";
        // ----------------------------------------------

        // 3.2. 扩展:遍历当前节点的所有邻居
        for (int neighbor : graph[current_node]) {
            // 3.3. 检查邻居节点是否【未被访问过】
            // find()返回end()迭代器表示元素不存在。
            if (visited.find(neighbor) == visited.end()) {
                // 3.4. 如果未访问,则标记并入队,准备在下一层处理
                visited.insert(neighbor);
                q.push(neighbor);
            }
        }
    }
}
  • 时间复杂度: O(V + E),其中 V 是图中顶点的数量,E 是边的数量。每个顶点入队和出队一次 (O(V)),每条边被访问一次 (O(E))。
  • 空间复杂度: O(V),在最坏情况下(例如星形图或完全图),队列可能需要存储所有顶点。visited 集合也需要 O(V) 的空间。

二、 应用一:无权图的最短路径

在所有边的权重都为 1 的图(无权图)中,从节点 A 到节点 B 的最短路径,就是包含边数最少的路径。

理论保证:BFS 算法的层序遍历特性,天然保证了它第一次到达任何一个节点 v 时,所经过的路径都是从起点 sv 的最短路径。这是因为 BFS 会先遍历完所有距离为 k 的节点,然后才会通过这些节点去触达距离为 k+1 的节点。

案例:走迷宫

问题描述:给定一个 M x N 的二维网格作为迷宫,其中 0 代表通路,1 代表墙壁。计算从起点 (start_row, start_col) 到终点 (end_row, end_col) 的最短移动步数。每次只能上、下、左、右移动一格。

函数设计与解释
  • 函数作用:
    精确计算在给定的二维迷宫中,从起点到终点的最短路径长度(即最少步数)。如果无法到达,则明确指出。
  • 使用格式模板:
    int shortestPathInMaze(
        const std::vector<std::vector<int>>& maze,
        const std::pair<int, int>& start,
        const std::pair<int, int>& end
    );
    
  • 参数含义:
    • const std::vector<std::vector<int>>& maze: 一个常量引用传递的二维 int 向量,代表迷宫地图。maze[r][c] == 0 为通路,== 1 为障碍。此设计高效且安全,保证原始数据不被修改。
    • const std::pair<int, int>& start: 代表起点 {行, 列} 坐标的 std::pair 对象。
    • const std::pair<int, int>& end: 代表终点 {行, 列} 坐标的 std::pair 对象。
  • 返回值:
    • int: 一个非负整数,表示最短路径的长度。如果起点即终点,返回 0
    • -1: 一个特定的错误码,表示在以下任一情况下无法找到路径:迷宫为空、起点或终点是障碍物、从起点无法到达终点。
代码实现

为了追踪步数,我们需要在队列的状态中增加一个“距离”维度。

#include <vector>
#include <queue>
#include <tuple> // 用于在队列中存储多个值

/**
 * @brief 计算二维迷宫中的最短路径长度。
 */
int shortestPathInMaze(const std::vector<std::vector<int>>& maze,
                       const std::pair<int, int>& start,
                       const std::pair<int, int>& end) {
    // --- 1. 预处理与边界检查 ---
    if (maze.empty() || maze[0].empty()) return -1;
    int rows = maze.size();
    int cols = maze[0].size();
    if (maze[start.first][start.second] == 1 || maze[end.first][end.second] == 1) {
        return -1;
    }
    if (start == end) return 0;

    // --- 2. BFS 初始化 ---
    // 队列存储状态: {row, col, distance}
    std::queue<std::tuple<int, int, int>> q;
    // visited 数组,比哈希表更高效,因为坐标是连续整数
    std::vector<std::vector<bool>> visited(rows, std::vector<bool>(cols, false));

    // 将起点入队,距离为 0
    q.emplace(start.first, start.second, 0);
    visited[start.first][start.second] = true;

    // 定义四个移动方向向量: 上, 下, 左, 右
    const int dr[] = {-1, 1, 0, 0};
    const int dc[] = {0, 0, -1, 1};

    // --- 3. BFS 主循环 ---
    while (!q.empty()) {
        auto [currentRow, currentCol, dist] = q.front();
        q.pop();

        // --- 4. 探索邻居节点 ---
        for (int i = 0; i < 4; ++i) {
            int nextRow = currentRow + dr[i];
            int nextCol = currentCol + dc[i];

            // 检查新坐标是否为终点
            if (nextRow == end.first && nextCol == end.second) {
                return dist + 1; // 找到了,返回总步数
            }

            // 检查新坐标的合法性
            if (nextRow >= 0 && nextRow < rows && nextCol >= 0 && nextCol < cols && // 边界检查
                maze[nextRow][nextCol] == 0 &&   // 通路检查
                !visited[nextRow][nextCol]) {    // 未访问检查
                
                // 标记为已访问并入队
                visited[nextRow][nextCol] = true;
                q.emplace(nextRow, nextCol, dist + 1);
            }
        }
    }

    // 如果队列处理完毕仍未找到终点,说明路径不存在
    return -1;
}

三、 应用二:树与图的层序遍历

层序遍历(Level-Order Traversal)要求按层级顺序访问节点,同一层内的节点则按从左到右的顺序访问。这与 BFS 的工作机制完全吻合。

核心技巧:为了将不同层的节点分开处理,我们在每一层的遍历开始前,记录下当前队列的大小 levelSize。这个值恰好是当前层所包含的节点数。随后,我们进行一个固定 levelSize 次的循环,这样就能确保在一次外层 while 循环中,只处理当前层的节点,并将下一层的节点全部入队。

案例:二叉树的层序遍历

问题描述:给定一个二叉树的根节点,返回其节点值的层序遍历结果,其中每一层的结果都存储在一个单独的向量中。

函数设计与解释
  • 函数作用:
    对一个二叉树进行层序遍历,并将结果组织成一个二维向量,清晰地反映树的层级结构。
  • 使用格式模板:
    // 定义二叉树节点结构
    struct TreeNode {
        int val;
        TreeNode *left;
        TreeNode *right;
        TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
    };
    
    std::vector<std::vector<int>> levelOrder(TreeNode* root);
    
  • 参数含义:
    • TreeNode* root: 指向二叉树根节点的指针。如果树为空,该指针为 nullptr
  • 返回值:
    • std::vector<std::vector<int>>: 一个二维向量。
      • 如果树为空,返回一个空的二维向量。
      • 否则,外层向量的第 i 个元素(一个 std::vector<int>)代表树的第 i 层(从 0 开始计数)所有节点的值,顺序为从左到右。
代码实现
#include <vector>
#include <queue>

struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

std::vector<std::vector<int>> levelOrder(TreeNode* root) {
    std::vector<std::vector<int>> result;
    if (!root) {
        return result; // 对空树的健壮性处理
    }

    std::queue<TreeNode*> q;
    q.push(root);

    while (!q.empty()) {
        // 1. 在处理新一层之前,获取当前队列的大小
        int levelSize = q.size();
        std::vector<int> currentLevel;
        currentLevel.reserve(levelSize); // 预分配内存,提高效率

        // 2. 循环 levelSize 次,恰好处理完当前层的所有节点
        for (int i = 0; i < levelSize; ++i) {
            TreeNode* node = q.front();
            q.pop();

            currentLevel.push_back(node->val);

            // 3. 将下一层的节点(非空子节点)加入队列
            if (node->left) {
                q.push(node->left);
            }
            if (node->right) {
                q.push(node->right);
            }
        }

        // 4. 将当前层的结果存入最终结果中
        result.push_back(currentLevel);
    }

    return result;
}

四、 BFS 的更多应用与延伸

BFS 的能力远不止于此,其思想是解决许多其他问题的基础。

  1. 寻找连通分量 (Connected Components):
    在一个无向图中,从任意一个未访问的节点开始进行 BFS,可以找到其所在的所有可达节点,这就构成了一个连通分量。重复此过程直到所有节点都被访问,即可找到图中所有的连通分量。

  2. 二分图判定 (Bipartite Graph Checking):
    一个图是二分图,当且仅当它不包含奇数长度的环。可以使用 BFS 进行“染色”:将起点染成颜色 A,其所有邻居染成颜色 B,邻居的邻居再染成颜色 A,以此类推。如果在染色过程中发现一个节点的邻居已经被染上了和自己相同的颜色,则说明存在奇数环,该图不是二分图。

  3. 拓扑排序 (Topological Sort - Kahn’s Algorithm):
    Kahn’s 算法是解决有向无环图 (DAG) 拓扑排序的经典方法,其核心就是 BFS。算法从所有入度为 0 的节点开始(相当于图的“起点”),将它们放入队列。每次从队列中取出一个节点,将其加入排序结果,并将其所有邻居的入度减 1。如果某个邻居的入度变为 0,则将其入队。这个过程完美地体现了 BFS 的逐层处理思想。

总结

广度优先搜索 (BFS) 是一种强大的算法,其精髓在于队列驱动的层序扩展

  • 实现基石std::queuevisited 集合是 BFS 实现的左膀右臂,前者保证了层序,后者避免了冗余。
  • 核心应用
    • 对于无权图最短路径问题,BFS 是不二之选,其找到的第一个解即为最优解。
    • 对于层序遍历,BFS 的内在机制与问题需求完美契合,通过 levelSize 技巧可轻松实现分层。
  • 思维模式:当你遇到诸如“最少步数”、“最短时间”、“最近距离”(在边权相等时)或需要按层级处理的问题时,应第一时间联想到 BFS。

如果觉得本文对您有所帮助,点个赞和关注吧,谢谢!!!你的支持就是我持续更新的最大动力

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

饭碗的彼岸one

感谢鼓励,谢谢

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值