广度优先搜索(BFS)

算法概述

【广度优先搜索】是一种图搜索算法,用于遍历或搜索图中的节点。它从起始节点开始,逐层向外扩展,直到找到目标节点或遍历完所有节点。BFS通常用于寻找最短路径、连通性等问题。

算法设计思路

广度优先搜索的核心思想是:使用队列来实现层次遍历,确保首先访问距离起点最近的节点,然后逐层向外扩展。其基本步骤如下:

  1. 将起始节点加入队列,并标记为已访问
  2. 当队列不为空时,重复以下步骤:
    • 从队列中取出一个节点
    • 访问该节点的所有未被访问的邻居节点,并将它们加入队列
  3. 直到找到目标节点或队列为空

代码实现与解析

基本BFS实现

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

using namespace std;

// 广度优先搜索函数
void bfs(const vector<vector<int>>& graph, int start) {
    queue<int> q; // 队列用于存储待访问的节点
    unordered_set<int> visited; // 集合用于记录已访问的节点

    q.push(start); // 将起始节点加入队列
    visited.insert(start); // 标记起始节点为已访问

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

        cout << "访问节点: " << node << endl;

        // 访问所有邻居节点
        for (int neighbor : graph[node]) {
            if (visited.find(neighbor) == visited.end()) { // 如果邻居节点未被访问
                q.push(neighbor); // 加入队列
                visited.insert(neighbor); // 标记为已访问
            }
        }
    }
}

int main() {
    // 示例图的邻接表表示
    vector<vector<int>> graph = {
        {1, 2},    // 节点0的邻居
        {0, 3, 4}, // 节点1的邻居
        {0, 4},    // 节点2的邻居
        {1, 5},    // 节点3的邻居
        {1, 2, 5}, // 节点4的邻居
        {3, 4}     // 节点5的邻居
    };

    bfs(graph, 0); // 从节点0开始进行广度优先搜索

    return 0;
}

时间复杂度分析

  • 时间复杂度:O(V + E),其中V是节点数,E是边数
  • 空间复杂度:O(V),需要存储队列和访问标记

优化策略

  1. 【双向BFS】当同时知道起点和终点时,可以从两端同时开始BFS,直到两个搜索相遇

    bool bidirectionalBFS(int start, int end) {
     if (start == end) return true;
    
     queue<int> q1, q2;
     unordered_set<int> visited1, visited2;
    
     q1.push(start);
     q2.push(end);
     visited1.insert(start);
     visited2.insert(end);
    
     while (!q1.empty() && !q2.empty()) {
         // 总是从较小的队列扩展
         if (q1.size() > q2.size()) {
             swap(q1, q2);
             swap(visited1, visited2);
         }
    
         int size = q1.size();
         for (int i = 0; i < size; i++) {
             int node = q1.front();
             q1.pop();
    
             for (int neighbor : graph[node]) {
                 if (visited1.count(neighbor)) continue; // 已访问
    
                 if (visited2.count(neighbor)) {
                     return true; // 两个搜索相遇
                 }
    
                 visited1.insert(neighbor);
                 q1.push(neighbor);
             }
         }
     }
    
     return false; // 无法到达
    }
    
  2. 【01BFS】处理边权为0或1的最短路径问题时,可以用双端队列优化

    vector<int> bfs01(vector<vector<pair<int, int>>>& graph, int start) {
     int n = graph.size();
     vector<int> dist(n, INT_MAX);
     deque<int> dq; // 使用双端队列
    
     dist[start] = 0;
     dq.push_front(start);
    
     while (!dq.empty()) {
         int node = dq.front();
         dq.pop_front();
    
         for (auto& [neighbor, weight] : graph[node]) {
             if (dist[neighbor] > dist[node] + weight) {
                 dist[neighbor] = dist[node] + weight;
    
                 if (weight == 0) {
                     dq.push_front(neighbor); // 权重为0的边,加入队首
                 } else {
                     dq.push_back(neighbor);  // 权重为1的边,加入队尾
                 }
             }
         }
     }
    
     return dist;
    }
    
  3. 【状态压缩BFS】当需要记录多个状态时,可以使用位运算进行状态压缩

    // 使用位运算表示状态,比如在迷宫中记录已访问的物品
    int bfsWithBitmask(vector<vector<int>>& grid, int startX, int startY) {
     int rows = grid.size();
     int cols = grid[0].size();
     int totalItems = 5; // 假设有5个物品需要收集
    
     // visited[x][y][state]表示在(x,y)位置,已收集物品状态为state时是否访问过
     vector<vector<vector<bool>>> visited(rows, vector<vector<bool>>(cols, vector<bool>(1 << totalItems, false)));
    
     queue<tuple<int, int, int>> q; // (x, y, state)
     q.push({startX, startY, 0});
     visited[startX][startY][0] = true;
    
     int steps = 0;
     vector<pair<int, int>> dirs = { {-1, 0}, {1, 0}, {0, -1}, {0, 1} };
    
     while (!q.empty()) {
         int size = q.size();
         for (int i = 0; i < size; i++) {
             auto [x, y, state] = q.front();
             q.pop();
    
             // 如果所有物品都收集了
             if (state == (1 << totalItems) - 1) {
                 return steps;
             }
    
             for (auto& dir : dirs) {
                 int nx = x + dir.first;
                 int ny = y + dir.second;
    
                 if (nx < 0 || nx >= rows || ny < 0 || ny >= cols || grid[nx][ny] == -1) continue;
    
                 int newState = state;
                 if (grid[nx][ny] > 0) {
                     // 收集物品,物品编号为grid[nx][ny]-1
                     newState |= (1 << (grid[nx][ny] - 1));
                 }
    
                 if (!visited[nx][ny][newState]) {
                     visited[nx][ny][newState] = true;
                     q.push({nx, ny, newState});
                 }
             }
         }
         steps++;
     }
    
     return -1; // 无法收集所有物品
    }
    

A*搜索算法(BFS的扩展)

A*算法是BFS的一种启发式扩展,它使用估价函数来引导搜索方向,优先探索更有希望到达目标的路径。

// A*算法寻找最短路径
vector<pair<int, int>> aStarSearch(vector<vector<int>>& grid, pair<int, int> start, pair<int, int> goal) {
    int rows = grid.size();
    int cols = grid[0].size();

    // 优先队列,按f值(g+h)排序,最小值在队首
    priority_queue<tuple<int, int, int>, vector<tuple<int, int, int>>, greater<>> pq;

    // g[r][c]表示从起点到(r,c)的已知最短距离
    vector<vector<int>> g(rows, vector<int>(cols, INT_MAX));

    // parent记录路径
    vector<vector<pair<int, int>>> parent(rows, vector<pair<int, int>>(cols));

    // 启发函数h:曼哈顿距离
    auto h = [&goal](int r, int c) -> int {
        return abs(r - goal.first) + abs(c - goal.second);
    };

    g[start.first][start.second] = 0;
    pq.push({h(start.first, start.second), start.first, start.second}); // {f, r, c}

    vector<pair<int, int>> dirs = { {-1, 0}, {1, 0}, {0, -1}, {0, 1} };

    while (!pq.empty()) {
        auto [f, r, c] = pq.top();
        pq.pop();

        // 达到目标
        if (r == goal.first && c == goal.second) {
            // 重建路径
            vector<pair<int, int>> path;
            for (auto pos = make_pair(r, c); pos != start; pos = parent[pos.first][pos.second]) {
                path.push_back(pos);
            }
            path.push_back(start);
            reverse(path.begin(), path.end());
            return path;
        }

        // f值已经不是最小值,跳过
        if (f > g[r][c] + h(r, c)) continue;

        for (auto& dir : dirs) {
            int nr = r + dir.first;
            int nc = c + dir.second;

            if (nr < 0 || nr >= rows || nc < 0 || nc >= cols || grid[nr][nc] == 1) continue;

            int ng = g[r][c] + 1; // 假设步长为1

            if (ng < g[nr][nc]) {
                g[nr][nc] = ng;
                parent[nr][nc] = {r, c};
                pq.push({ng + h(nr, nc), nr, nc});
            }
        }
    }

    return {}; // 无法到达目标
}

练习题推荐

  1. LeetCode 934: 最短的桥(多源BFS)
  2. LeetCode 994: 腐烂的橘子(多源BFS模拟)
  3. LeetCode 1162: 地图分析(多源BFS计算距离)
  4. LeetCode 127: 单词接龙(变形BFS)
  5. LeetCode 752: 打开转盘锁(状态BFS)
  6. LeetCode 815: 公交路线(图的BFS)
  7. LeetCode 773: 滑动谜题(状态压缩BFS)
  8. POJ 3984: 迷宫问题(基础BFS路径记录)
  9. CodeForces 580C: Kefa and Park(带条件BFS)

总结

广度优先搜索是解决"最短路径"类问题的首选算法,尤其在无权图或等权图中。它的核心思想是"层次遍历",确保首先探索距离起点近的节点,然后逐渐向外扩展。

BFS的关键优势:

  1. 保证找到的路径是最短的(在无权图中)
  2. 适合处理最短路径、最少步数等问题
  3. 能够有效处理大型状态空间

BFS的主要挑战:

  1. 空间复杂度高,需要存储整个前沿
  2. 在处理大型图或高维状态空间时,队列可能会变得很大

在竞赛编程中,熟练掌握BFS的变体(如双向BFS、A*搜索、01BFS等)能够帮助你高效解决各类搜索问题。