BFS算法框架
BFS的核心思想,就是把一些問題抽象成圖,從一個節點開始,向四周擴散。一般來說,寫BFS都是用[隊列]這個數據結構,每次將一個節點周圍的節點加入到隊尾。
BFS相對於DFS的最主要區別是:BFS找到的路徑一定是最短的,但代價就是空間復雜度比DFS大很多。本文從兩道經典的BFS題目來講解。
先舉例⼀下 BFS 出現的常⻅場景好吧, 問題的本質就是讓你在⼀幅「圖」 中找到從起點 start 到終點 target 的最近距離, 這個例⼦聽起來很枯燥, 但是 BFS 算法問題其實都是在⼲這個事⼉, 把枯燥的本質搞清楚了, 再去欣賞各種問題的包裝才能胸有成竹嘛。
-
⽐如⾛迷宮, 有的格⼦是圍牆不能⾛, 從起點到終點的最短距離是多少?如果這個迷宮帶「傳送門」 可以瞬間傳送呢?
-
⽐如說兩個單詞, 要求你通過某些替換, 把其中⼀個變成另⼀個, 每次只能替換⼀個字符, 最少要替換⼏次?
-
⽐如說連連看游戲, 兩個⽅塊消除的條件不僅僅是圖案相同, 還得保證兩個⽅塊之間的最短連線不能多於兩個拐點。 你玩連連看, 點擊兩個坐標, 游戲是如何判斷它倆的最短連線有⼏個拐點的?
這些問題都沒啥奇技淫巧, 本質上就是⼀幅「圖」 , 讓你從⼀個起點, ⾛到終點, 問最短路徑。 這就是 BFS 的本質, 框架搞清楚了直接默寫就好。
BFS框架:
int BFS(Node start, Node target){
// 核心數據結果:隊列
queue<Node> q;
// 標記集合,避免走回頭路
set<Node> visited;
// 首先將起點加入隊尾
q.push(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 is target)
return step;
// 將當前節點cur的所有相鄰節點加入隊尾
for(Node p : cur.adj()){
if(p not in visited){
// 加入隊尾
q.push(p);
// 標記
visited.add(p);
}
}
}
// 划重點,更新步數在這里
step++;
}
}
其中,cur.adj()
表示節點cur
的相鄰節點(圖即鄰接點,樹即子節點)。
經典例題一:二叉樹的最小高度(leetcode.111)
怎么套到 BFS 的框架⾥呢?⾸先明確⼀下起點 start 和終點 target 是什么,怎么判斷到達了終點?顯然起點就是 root 根節點, 終點就是最靠近根節點的那個「葉⼦節點」嘛, 葉⼦節點就是兩個⼦節點都是 null 的節點:
if(cur->left == nullprt && cur->right == nullptr)
//到達葉子結點,返回
那么,按照BFS框架稍加改造:
#include <queue>
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
int minDepth(TreeNode* root) {
if(root == nullptr)
return 0;
queue<TreeNode*> q;
// 將起點加入到隊尾
q.push(root);
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.752)
直接上代碼:
#include <string>
#include <queue>
class Solution {
public:
// 將s[i]向上撥
string plusOne(string s, int i){
if(s[i] =='9')
s[i] = '0';
else
s[i] = s[i] + 1;
return s;
}
// 將s[i]向下撥
string minusOne(string s, int i){
if(s[i] == '0')
s[i] = '9';
else
s[i] = s[i] - 1;
return s;
}
int BFS(vector<string> &deadends, string target){
queue<string> q;
set<string> visited;
set<string> deads;
for(int i = 0; i < deadends.size(); i++)
deads.insert(deadends[i]);
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.find(cur) != deads.end())
continue;
if(cur == target){
printf("%d\n", step);
return step;
}
// 將該結點的所有子節點加入隊尾
for(int i = 0; i < 4; i++){
string adjPlus = plusOne(cur, i);
string adjMinus = minusOne(cur, i);
if(visited.find(adjPlus) == visited.end()){
q.push(adjPlus);
visited.insert(adjPlus);
}
if(visited.find(adjMinus) == visited.end()){
q.push(adjMinus);
visited.insert(adjMinus);
}
}
}
step++;
}
return -1;
}
int openLock(vector<string>& deadends, string target) {
return BFS(deadends, target);
}
};