前言
最近刷题,凑巧刷到一些关于BFS的题,所以顺便总结一下BFS的解题方法。该博客面向新手小白,从入门到熟练BFS。如有不当的地方,也请真正精通BFS的大佬指正!
具体的内容会分为三个板块,以二叉树的层序遍历为引子,介绍BFS的原理,最后是多源BFS问题。具体的算法题会先描述解题思路,然后提供我的参考代码。
二叉树的层序遍历
二叉树的层序遍历与常规三件套,前、中、后序遍历有所不同,个人认为层序遍历理解起来更直观。其核心思想为“老带新”,使用一个队列(先进先出)达成此目的。
-
如动图所示,我们从根节点开始遍历,首先初始化队列,将节点“1”入队列。
-
当节点“1”出队列的时候,对节点进行操作,同时让节点“1”的左右孩子,节点“2”,“3”入队列。【老节点将新节点带入队列】
-
反复循环入队列,出队列操作。如果某个节点的左节点(右节点)为空,就不进行入队列操作。
-
直到队列为空的时候,我们就对一棵二叉树完成了遍历。
【多叉树的层序遍历原理相同】
void levelorder(TreeNode* root){
if(root == nullptr)
return;
queue<TreeNode> level;
level.push(root);
while(!level.empty()){
TreeNode* temp = level.front();
level.pop();
//对节点temp进行操作
cout << temp->val << ' ';
//对孩子进行入队列操作
if(temp->left){
level.push(root->left);
}
if(temp->right){
level.push(root->right);
}
}
}
如果在遍历的时候需要考虑数据来自于哪一层,上述代码就失效了,因为队列中可能同时存在来自两层的节点,这时可以加入一个cot
用以计数,就能解决问题。
102. 二叉树的层序遍历 - 力扣(LeetCode)
题目要求很简单,层序遍历二叉树,最终以二维数组的形式进行输出。思路便是上文提到的,在开始出队列操作前,使用一个cot
记录队列中的节点数量(即属于同样深度的节点的数量),while(cot--)
保证一个数组内的数都来自同一层。
vector<vector<int>> levelOrder(TreeNode* root) {
if(root==nullptr)
return {};
queue<TreeNode*> bfs;
bfs.push(root);
int cot=-1;
vector<vector<int>> ans;
while(!bfs.empty()){
cot=bfs.size(); //记录一层的节点数量
ans.resize(ans.size()+1,vector<int>(cot,0));
int i=0; //用以控制插入数组的列的位置
while(cot--){
TreeNode* temp=bfs.front();
bfs.pop();
if(temp->left){
bfs.push(temp->left);
}
if(temp->right){
bfs.push(temp->right);
}
ans[ans.size()-1][i++]=temp->val;
}
}
return ans;
}
当然,你也可以尝试再使用一个队列,在节点入队列的时候记录它来自哪一层,也可以达到相同目的。如果感兴趣,可以自己尝试实现以下。不过这样做的话,空间复杂度就是O(N)。
BFS
真正的BFS算法更多是运用于图的一种搜索策略,但是其实际思想仍然是上文介绍的“老带新”,只不过因为图本身的结构特性,我们在还需要一个布尔数组记录某个节点的状态(是否被搜素过),以避免重复地搜索。做两道题,你就能明白是怎么回事了:)
1306. 跳跃游戏 III - 力扣(LeetCode)
该题是对BFS算法的基础运用,分析题目后你会发现,它实际上和二叉树的结构很相似:要么向左跳跃(左孩子),要么向右跳跃(右孩子),或者无法跳跃(孩子为空,nullptr)。所以直接采用层序遍历的策略进行遍历,但是可能会出现跳到已经跳过的格子上,所以加一个与原数组相同大小的布尔数组以记录格子状态。
我的参考代码如下:
bool bfs(vector<int> arr, int loc){
vector<bool> status(arr.size(),false); //用以记录状态,被搜索过状态变为true
queue<int> temp;
temp.push(loc);
status[loc]=true;
while(!temp.empty()){
int size=temp.size();//记录一层的节点数量
while(size--){
int cur=temp.front();
temp.pop();
if(arr[cur]==0)
return true; //满足题意则返回true
//跳跃到左边的节点入队列
if(cur-arr[cur]>=0 && status[cur-arr[cur]] == false){
status[cur-arr[cur]]=true;
temp.push(cur-arr[cur]);
}
//跳跃到右边的节点入队列
if(cur+arr[cur]<arr.size() && status[cur+arr[cur]] == false){
status[cur+arr[cur]]=true;
temp.push(cur+arr[cur]);
}
}
}
return false;
}
bool canReach(vector<int>& arr, int start) {
return bfs(arr,start);
}
总结一下,BFS的基本套路:
- 初始化:创建一个队列来存储待访问的节点,并将起始节点加入队列。
- 记录状态:创建一个相同大小的布尔数组记录节点的状态;一个
cot
计数器,记录同层的节点数量。 - 访问节点:出队列操作,对节点进行操作。
- 入队列:判断节点邻居的状态,将它们入队列,并更改状态。
- 重复上述操作,直到所有节点已经被遍历。
多源BFS
多源BFS本质上并没有比BFS难,只是要同时处理多个格子的状态,处理好不同源之间的邻居关系,不能重复入队列或漏入队列即可。
1162. 地图分析 - 力扣(LeetCode)
我们在BFS中遍历的层数就是该题所求的曼哈顿距离,稍微改变BFS的一些细节就能解决这道题。
如图解所示,初始化队列的时候,将四角的陆地都入队列;第一轮遍历,遍历四个浅色格子;第二轮遍历,遍历到中间的深蓝色格子。总共遍历了两层格子,所以最终的答案就是2。
我的参考代码如下:
int maxDistance(vector<vector<int>>& grid) {
struct loc {
int x;
int y;
};
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};
const int N = grid.size();
vector<vector<bool>> statu(N, vector<bool>(N, false));
queue<loc> land;
// 遍历grid,将初始的陆地单元格进入队列
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
if (grid[i][j] == 1) {
land.push(loc(i, j));
statu[i][j] = true; // 入队列后就更新该格子的状态
}
}
}
// 判断是否只有陆地和海洋
if (land.size() == N * N || land.size() == 0)
return -1;
int cot = 0;
while (!land.empty()) {
cot++;
int temp_size = land.size();
// 让一层的陆地都出队列
while (temp_size--) {
loc node(land.front());
land.pop();
// 二维矩阵(图)的邻居是上下左右的四个格子
for (int i = 0; i < 4; i++) {
int x1 = node.x + dx[i];
int y1 = node.y + dy[i];
//判断节点状态,以及坐标的合法性
if (x1 >= 0 && x1 < N && y1 >= 0 && y1 < N &&
statu[x1][y1] == false) {
land.push(loc(x1, y1));
statu[x1][y1] = true;
}
}
}
}
return --cot;
你可以用下面这道题检验一下自己,看有没有真的学会BFS!
感谢你能看到这里,谢谢!