BFS例题梳理

284 阅读8分钟

BFS基本原理

广度优先搜索(Breadth-First Search,简称BFS)是一种图遍历算法,广泛应用于寻找最短路径、解决迷宫问题、图论中的连通性问题等。

BFS的工作原理

问题的本质:让你在一幅「图」中找到从起点 start 到终点 target 的最近距离。

常见场景:这些问题的本质都是在图上寻找「最短路径」,只是题目的形式不同。

  • 迷宫最短路径:起点到终点的最短路径,有的格子是围墙不能走,甚至带有「传送门」。
  • 单词变换:两个单词间每次替换一个字符,求最少替换次数,使一个单词变成另一个。
  • 连连看:判断两个相同图案的方块之间最短路径的拐点数,若少于两个拐点则可消除。

BFS的核心思想:从起点开始,沿着图的每一层逐层展开搜索,直到找到目标节点。其特点是「逐层扫描」,与深度优先搜索(DFS)的「深度探索」不同。

  • 遍历方式:BFS使用队列(Queue)来存储当前正在访问的节点。
  • 目标:从起点开始,依次访问邻近的节点,每访问一层就扩展下一层,直到找到目标节点或遍历完整个图。
  • 最短路径:在无权图中,BFS保证找到从起点到目标节点的最短路径。

BFS的具体步骤(模板)

1000015687.jpg

graph LR
    A[初始化存储相邻节点的数据结构 q 和 visit] --> B[加入第一个节点,开始遍历]
    B --> C[循环遍历每一圈的节点]
    C --> D[返回判断:第i圈的x节点是否为目标节点]
    D --> |是| E[返回步数]
    D --> |不是| F[遍历第i圈x节点的相邻节点 adj]
    F --> G[判断是否为回头路]
    G --> |是| F
    G --> |不是| H[将邻居节点加入队列 q]
    H --> I[将邻居节点标记为已访问 visit]
    I --> J[步数 i++]
    J --> C
    C --> |队列为空| K[没找到目标节点]
  • 流程

    • 【初始化】初始化「存储」相邻的所有可选节点的数据结构q和visit

      • 队列(Queue) :用来存储当前层的所有节点。
      • 访问记录(Visited Set) :用来记录已经访问过的节点,避免重复访问。
      • 起点入队:将起点加入队列,并标记为已访问。
    • 【逐层扩展】

      • 加入「第一个节点」,开始遍历

      • 【循环】【遍历】围绕着「第一个节点」的「第i圈的每个节点」。

        • 出队节点:从队列中取出当前节点,检查它是否是目标节点。

          • 【返回判断】【找到目标节点】若当前第i圈的「x节点」是目标节点,则返回
        • 访问邻居节点:遍历当前第i圈的「x节点」的所有可选邻居节点adj,若邻居节点未访问过,则将其加入队列。

          • visit判断是不是「回头路」
        • 重复:继续逐层展开,直到找到目标节点或队列为空。 【i++】已走过的路径长度i++

    • 【返回判断】【队列为空】 :若队列为空,说明图中不存在目标节点。

int BFS(Node start, Node target) {
		// 【初始化数据结构】
    queue<Node> q; // 【缓存队列】队列 q 用于存储当前层的节点
    set<Node> visited; // 【备忘录】集合 visited 用于标记访问过的节点,避免重复遍历
    
    // 【入队初始节点】
    q.push(start); // 将起点节点加入队列
    visited.insert(start); // 标记起点已访问
		
		// 【循环遍历每层节点】
    while (!q.empty()) {
        int sz = q.size(); // 记录当前层节点数
        for (int i = 0; i < sz; i++) {
		        // 【出队,检查是否到达目标节点】
            Node cur = q.front();// 取出当前节点 cur
            q.pop(); // 将该节点出队,表示当前节点已被处理
            if (cur == target) // 返回 step,即到达目标节点的步数
                return step;
            // 【遍历当前节点的相邻节点】
            for (Node x : cur.adj()) { // cur.adj()泛指 cur 相邻的节点
		            // 判断邻居节点是否在 visited 中,如果没有被访问过
                if (visited.count(x) == 0) {
                    q.push(x);
                    visited.insert(x);
                }
            }
        }
    }
    // 如果走到这里,说明在图中【没有找到目标节点】
}

BFS的时空复杂度

特点:齐头并进的面扫描,bfs找最短路径时间复杂度比dfs低,但是空间复杂度高

  • 时间复杂度:BFS的时间复杂度通常是 O(V + E),其中 V 是节点的数量,E 是边的数量。BFS会遍历每一个节点和每一条边一次。
  • 空间复杂度:BFS的空间复杂度主要取决于队列的大小,最坏情况下是 O(V),当图中有很多节点需要存储在队列中时,空间复杂度较高。

例题

示例 1:求迷宫的最短路径

假设我们在一个二维迷宫中,起点为start,终点为target,而迷宫中有一些墙壁(无法通行)。我们希望找到从starttarget的最短路径。

通过BFS逐层扫描,从starttarget的最短路径为4步,BFS保证能够找到最短路径。

    #include <iostream>
    #include <queue>
    #include <set>
    using namespace std;

    struct Node {
        int x, y;
        Node(int x, int y) : x(x), y(y) {}
        // 获取相邻的节点,假设是上下左右四个方向
        vector<Node> adj() {
            return {Node(x+1, y), Node(x-1, y), Node(x, y+1), Node(x, y-1)};
        }
    };

    int BFS(Node start, Node target) {
        queue<Node> q;
        set<Node> visited;
        q.push(start);
        visited.insert(start);
        int step = 0;

        while (!q.empty()) {
            int sz = q.size();
            for (int i = 0; i < sz; i++) {
                Node cur = q.front(); q.pop();
                if (cur.x == target.x && cur.y == target.y)
                    return step;

                for (Node neighbor : cur.adj()) {
                    if (visited.count(neighbor) == 0) {
                        q.push(neighbor);
                        visited.insert(neighbor);
                    }
                }
            }
            step++;
        }
        return -1;  // 如果没有找到目标
    }

    int main() {
        Node start(0, 0), target(4, 4);
        cout << "The shortest path length is: " << BFS(start, target) << endl;
        return 0;
    }

示例 2:单词变换(Word Ladder)

在此问题中,给定两个单词,要求每次只能修改一个字母,求最短的修改次数使得一个单词变成另一个。

解释:

  • BFS逐层遍历所有可能的单词变换,保证了找到最短路径。
    #include <iostream>
    #include <queue>
    #include <unordered_set>
    #include <string>
    using namespace std;

    int wordLadder(string beginWord, string endWord, unordered_set<string>& wordList) {
        if (wordList.find(endWord) == wordList.end()) return 0;

        queue<string> q;
        q.push(beginWord);
        int step = 1;

        while (!q.empty()) {
            int sz = q.size();
            for (int i = 0; i < sz; i++) {
                string word = q.front(); q.pop();
                if (word == endWord) return step;

                for (int j = 0; j < word.length(); j++) {
                    char original = word[j];
                    for (char c = 'a'; c <= 'z'; c++) {
                        word[j] = c;
                        if (wordList.find(word) != wordList.end()) {
                            q.push(word);
                            wordList.erase(word);  // Mark as visited
                        }
                    }
                    word[j] = original;
                }
            }
            step++;
        }
        return 0;  // No possible transformation
    }

    int main() {
        unordered_set<string> wordList = {"hot", "dot", "dog", "lot", "log"};
        cout << "The shortest transformation length is: " << wordLadder("hit", "cog", wordList) << endl;
        return 0;
    }

111. 二叉树的最小深度 - 力扣(LeetCode)

class Solution {
public:
    int minDepth(TreeNode* root) {
        if (root == nullptr) return 0;
        queue<TreeNode*> q;
        q.push(root);
        // root 本身就是一层,depth 初始化为 1
        int depth = 1;

        while (!q.empty()) {
            int sz = q.size();
            // 将当前队列中的所有节点向四周扩散
            for (int i = 0; i < sz; i++) {
                TreeNode* cur = q.front();
                q.pop();
                // 判断是否到达终点
                if (cur->left == nullptr && cur->right == nullptr) 
                    return depth;
                // 将 cur 的相邻节点加入队列
                if (cur->left != nullptr)
                    q.push(cur->left);
                if (cur->right != nullptr) 
                    q.push(cur->right);
            }
            // 这里增加步数
            depth++;
        }
        return depth;
    }
};

leetcode.cn/problems/op…

image.png

class Solution {
public:
    int openLock(vector<string>& deadends, string target) {
        // 记录需要跳过的死亡密码
        unordered_set<string> deads(deadends.begin(), deadends.end());
        // 记录已经穷举过的密码,防止走回头路
        unordered_set<string> visited;
        queue<string> q;
        // 从起点开始启动广度优先搜索
        int step = 0;
        q.push("0000");
        visited.insert("0000");

        while (!q.empty()) {
            int sz = q.size();
            // 将当前队列中的所有节点向周围扩散
            for (int i = 0; i < sz; i++) {
                string cur = q.front(); q.pop();

                // 判断是否到达终点
                if (deads.count(cur))
                    continue;
                if (cur == target)
                    return step;

                // 将一个节点的未遍历相邻节点加入队列
                for (int j = 0; j < 4; j++) {
                    string up = plusOne(cur, j);
                    if (!visited.count(up)) {
                        q.push(up);
                        visited.insert(up);
                    }
                    string down = minusOne(cur, j);
                    if (!visited.count(down)) {
                        q.push(down);
                        visited.insert(down);
                    }
                }
            }
            // 在这里增加步数
            step++;
        }
         // 如果穷举完都没找到目标密码,那就是找不到了
        return -1;
    }

    // 将 s[j] 向上拨动一次
    string plusOne(string s, int j) {
        s[j] = s[j] == '9' ? '0' : s[j] + 1;
        return s;
    }

    // 将 s[i] 向下拨动一次
    string minusOne(string s, int j) {
        s[j] = s[j] == '0' ? '9' : s[j] - 1;
        return s;
    }
};

双向BFS优化

image.png

  • 【停止条件】由【找到目标】变为【q1的元素在q2中有重叠】

  • 【交换缓存的集合】总是选择扩散个数少的那个q作为优先遍历的对象

#include <cassert>
class Solution {
public:

    string plusOne(string s, int j) {
        s[j] = s[j] == '9' ? '0': s[j] + 1;
        return s;
    }

    string minusOne(string s, int j) {
        s[j] = s[j] == '0' ? '9': s[j] - 1;
        return s;
    }

    int BFS(vector<string>& deadends, string target) {
        unordered_set<string> q1, q2;
        unordered_set<string> temp;
        unordered_set<string> deads(deadends.begin(), deadends.end());
        auto _q1 = make_unique<unordered_set<string>>(q1);
        auto _q2 = make_unique<unordered_set<string>>(q2);
        auto _t = make_unique<unordered_set<string>>(temp);
        // 记录已经穷举过的密码,防止走回头路
        unordered_set<string> visited;
        int cnt = 0;
        _q1->insert("0000");
        _q2->insert(target);
        assert(!_q1->empty());
        assert(!_q2->empty());

        while((!_q1->empty()) && (!_q2->empty())) {
            assert(_t->empty());
            for (auto& cur:*_q1) {
                // 检查是否到达终点
                if (_q2->count(cur) != 0) {
                    return cnt;
                }
                if (deads.count(cur) != 0) {
                    continue;
                }
                visited.insert(cur);

                // 【遍历当前节点的相邻节点】
                for (int j = 0; j < 4; j++) {
                    // 判断邻居节点是否在 visited 中,如果没有被访问过
                    string m = minusOne(cur, j);
                    if (visited.count(m)==0) {
                        _t->insert(m);
                   }
                    string p = plusOne(cur, j);
                    if (visited.count(p)==0) {
                        _t->insert(p);
                   }
                }

            }
            _q1->clear();
            _t.swap(_q1);
            if (_q1->size() > _q2->size()) {
                _q1.swap(_q2);
                assert(!_q1->empty());
            }
            assert(_t->empty());
            // assert(!_q2->empty());
            cnt++;
        }
        // 没找到终点
        return -1;
    }

    int openLock(vector<string>& deadends, string target) {
        return BFS(deadends, target);
    }
};

计算从位置 x 到 y 的最少步数 - MarsCode

#include <iostream>
#include <queue>
#include <set>
using namespace std;

int solution(int xPosition, int yPosition) {
  if (xPosition == yPosition) {
    return 0;
  }

  queue<pair<int, int>> q;
  set<pair<int, int>> visited;
  // 首末两步的步长必须是 1,每次移动的步长只能变化-1~1之间
  vector<int> choices({-1, 0, 1});
  int step = xPosition > yPosition ? -1 : 1, x = xPosition + step;
  int cnt = 1;

  q.push({x, step});
  visited.insert({x, step});

  // 遍历列表
  while (!q.empty()) {
    int sz = q.size();
    for (int i = 0; i < sz; i++) {
      // 【出队,检查是否达到目标节点】
      x = q.front().first;
      step = q.front().second;
      q.pop();

      // 【判断】
      if (x == yPosition && (step == 1 || step == -1)) {
        return cnt;
      }

      // 查询-1 0 1三个选项
      for (auto choice : choices) {
        // 没有被遍历过
        int next_step = step + choice;
        int next_pos = x + next_step;
        if (visited.count({next_pos, next_step}) == 0) {
          q.push({next_pos, next_step});
          visited.insert({next_pos, next_step});
        }
      }
    }
    // 递增
    cnt++;
  }

  return -1;
}

int main() {
  //  You can add more test cases here
  std::cout << (solution(12, 6) == 4) << std::endl;
  std::cout << (solution(34, 45) == 6) << std::endl;
  std::cout << (solution(50, 30) == 8) << std::endl;
  return 0;
}

数学

from collections import deque
import math

def solution(x_position, y_position):
    D = abs(y_position - x_position)
    if D == 0:
        return 0
    k = int(math.sqrt(D))
    if D == k * k:
        return 2 * k - 1
    elif D <= k * k + k:
        return 2 * k
    else:
        return 2 * k + 1

if __name__ == "__main__":
    #  You can add more test cases here
    print(solution(12, 6) == 4 )
    print(solution(34, 45) == 6)
    print(solution(50, 30) == 8)