BFS基本原理
广度优先搜索(Breadth-First Search,简称BFS)是一种图遍历算法,广泛应用于寻找最短路径、解决迷宫问题、图论中的连通性问题等。
BFS的工作原理
问题的本质:让你在一幅「图」中找到从起点 start 到终点 target 的最近距离。
常见场景:这些问题的本质都是在图上寻找「最短路径」,只是题目的形式不同。
- 迷宫最短路径:起点到终点的最短路径,有的格子是围墙不能走,甚至带有「传送门」。
- 单词变换:两个单词间每次替换一个字符,求最少替换次数,使一个单词变成另一个。
- 连连看:判断两个相同图案的方块之间最短路径的拐点数,若少于两个拐点则可消除。
BFS的核心思想:从起点开始,沿着图的每一层逐层展开搜索,直到找到目标节点。其特点是「逐层扫描」,与深度优先搜索(DFS)的「深度探索」不同。
- 遍历方式:BFS使用队列(Queue)来存储当前正在访问的节点。
- 目标:从起点开始,依次访问邻近的节点,每访问一层就扩展下一层,直到找到目标节点或遍历完整个图。
- 最短路径:在无权图中,BFS保证找到从起点到目标节点的最短路径。
BFS的具体步骤(模板)
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,而迷宫中有一些墙壁(无法通行)。我们希望找到从start到target的最短路径。
通过BFS逐层扫描,从start到target的最短路径为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…
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优化
-
【停止条件】由【找到目标】变为【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)