一篇文章教会你广度优先搜索
✂️✂️✂️
BFS概述
上一章【算法入门】最短时间学会DFS深度优先搜索中到了DFS和BFS的差异,其中我们能够看到BFS这种遍历方式对于迷宫问题的求解其实是非常合适的,接下来先带看看最基本迷宫问题当中如何使用BFS来实现
预备知识:学习BFS,用BFS走迷宫
💡💡💡💡💡
类似这种我们从起点走到终点,我们BFS通常是使用队列实现的,我们将一开始的起点入队列之后,我们从队列取出所有结点,再将他们的上下左右结点带入队列,这样就可以实现BFS深度遍历了!!
示意图:三种情况,图中有标识注意的三点
分析上图:所以我们就可以看出,当我们要走这个迷宫的时候,要处理第一种,我们要提前判断坐标是否在我们开的数组当中。第二种,我们遇到障碍物就不用入队列的操作,第三种:走过的点不能再走,我们就弄一个标记矩阵(vector<vector>visited)来标识我们走过的点,对于走过的点也不入队列,原因:当我们不将新元素入队列说明这个点已经走到头了
提示:这里的坐标我们选择用node进行存储,这里实现的是是否能从起点 到终点的接口,大家可以复制下面代码到自己的编辑器下跑一跑,下面是我测试的图:
//grid为0表示无障碍物,为1表示有障碍物
vector<vector<int>> grid
{ {0,0,1,1,1}
,{0,0,0,1,0}
,{0,0,0,1,0}
,{0,1,1,1,1}
,{0,0,0,0,0} };
#include<queue>
#include<stdbool.h>
int arr[4][2] = { {1,0},{-1,0},{0,-1},{0,1} };
struct node
{
//对于结点存储的坐标进行初始化
node(int x, int y)
:_x(x),
_y(y)
{}
int _x;
int _y;
};
//能够到返回1,不能返回0
bool BFS(vector<vector<int>>& visited, vector<vector<int>> grid, int startx, int starty,int destx,int desty, int row, int col)
{
//如果起点和终点是同一个点直接返回
if (startx == destx && starty == desty)
return true;
//我们先对起点进行入队列操作
//这里照顾java的同学所以使用node存储结点,这里用pair存储结点也是一样的
queue<node> q;
q.push(node(startx, starty));
//并且将已经入队列的点进行初始化
visited[startx][starty] = 1;
while (!q.empty())
{
//入当前队列所有结点的上下左右坐标,并将队列开始时有的结点出出去
int sz = q.size();
//将所有队列当中的元素删除,并入上他们周围四个点的坐标
for (int i = 0; i < sz; ++i)
{
//将当队头元素的周围四个坐标入队列,再删除队头元素
node tmp = q.front();
q.pop();
//处理当前点的周围四个点
for (int j = 0; j < 4; ++j)
{
//方向矩阵,不知道的请看看上一章的DFS讲解
int newX = tmp._x + arr[j][0];
int newY = tmp._y + arr[j][1];
//坐标越界,不合法,过滤
if (newX < 0 || newX >= row || newY < 0 || newY >= col)
continue;
//没有访问过并且不是障碍物的点入队列
if (visited[newX][newY] == 0 && grid[newX][newY] == 0)
{
//如果入的点是我们的终点我们就返回true,注意倘若终点若是障碍物,我们也走不到这里,最终也会返回false,走不到
if (newX == destx && newY == desty)
return true;
//入的点不是就进行,入队列并且对点做标记
q.push(node(newX, newY));
visited[newX][newY] = 1;
}
}
}
}
//队列出完了都没有找到(destx,desty)表示这个点访问不到
return false;
}
//测试函数
int main()
{
while (1)
{
//实现的是一个输入起点和终点,返回是否能否到的迷宫
int n = 5;
cout << "请输入起点坐标 sx,sy 以及终点坐标 dx,dy\n";
int startx, starty, destx, desty;
cin >> startx >> starty >> destx >> desty;
//设置我们grid当中的障碍物
//grid为0表示无障碍物,为1表示有障碍物
vector<vector<int>> grid
{ {0,0,1,1,1}
,{0,0,0,1,0}
,{0,0,0,1,0}
,{0,1,1,1,1}
,{0,0,0,0,0} };
//初始化标记矩阵
vector<vector<int>> visited(n, vector<int>(n, 0));
cout<<BFS(visited, grid, startx, starty, destx, desty, n, n);
}
return 0;
}
看完上面我们就可以对BFS做一个小总结:
BFS()
{
1.初始步骤,初始化队列(将首元素入队列)
2.遍历队列中的每一种可能,while(队列不为空)
{
通过当前的队头元素带出下一步的可能,依次入队列
{
判断是否满足,按照要求进行逻辑处理
}
继续遍历队列中的剩余情况
}
}
讲到这里听的懂的老哥就可以走到我们的下一步了,做几道练习题来实验实验吧。
习题一: 员工的重要性
📝📝📝
员工的重要性
看过我写的DFS的老哥就知道这题之前写过,现在我们再尝试用BFS的方式写写这道题目
/*
// Definition for Employee.
class Employee {
public:
int id;
int importance;
vector<int> subordinates;
};
*/
class Solution {
public:
int getImportance(vector<Employee*> employees, int id) {
}
};
思路:我们要求出当前id的这个人的下属以及他的下属的重要度,所以我们可以用一个queue<Employee*>用来要求的id的结构体指针,这样子我们就可以通过访问他的subordinates,依次将他的下属入队列,将所有的重要度都加起来返回就可以了,因为这里我们有用到id 找到对应雇员的结构体指针,所以我们用个哈希表(um)来帮助我们查找
class Solution {
public:
int BFS(unordered_map<int,Employee*>& um,int id)
{
//初始化队列
queue<Employee*> q;
q.push(um[id]);
int importance = 0;
//遍历队列
while(!q.empty())
{
//将当前队列中的每一个id对应的雇员的重要度相加,将他们手下的雇员入队列
//重复这个过程
int sz = q.size();
//计算当前队列中的数据,并且依次取出
for(int i =0;i<sz;++i)
{
//每次取队头数据
Employee* front = q.front();
q.pop();
importance+=front->importance;
//再将出掉的队头数据的直系下属入队
for(int subid: front->subordinates)
{
q.push(um[subid]);
}
}
}
return importance;
}
int getImportance(vector<Employee*> employees, int id) {
unordered_map<int,Employee*> um;
//哈希表建立id与雇员的映射
for(Employee* e: employees)
{
um[e->id] = e;
}
//这里我们可以写一个接口
return BFS(um,id);
}
};
没看懂的同学可以再看看这张图,应该就会比较清晰了
第一步:
第二步: 体现for循环的作用
看完这里的你,理解完第一题,已经就成功了一大半了,让我们继续把接下来的题干完吧!!!
习题二: N叉树的层序遍历
📝📝📝
429. N 叉树的层序遍历
大家如果学习过二叉树的话想比都写过:二叉树的层序遍历这样的题,实际上N叉树也是大同小异的,图解:
// Definition for a Node.
class Node {
public:
int val;
vector<Node*> children;
Node() {}
Node(int _val) {
val = _val;
}
Node(int _val, vector<Node*> _children) {
val = _val;
children = _children;
}
};
*/
class Solution {
public:
vector<vector<int>> levelOrder(Node* root) {
}
};
分析:他这里给我们二叉树的头,并且我们可以通过vector<Node*> children,表示我们可以拿到当前头节点指针他所有的孩子,他要求我们返回的是二维数组,那我们这题也试试用BFS来解决
class Solution {
public:
vector<vector<int>> levelOrder(Node* root) {
if(root==NULL)
return vector<vector<int>>();
//初始化队列
queue<Node*> q;
q.push(root);
//用于返回的retvv
vector<vector<int>> retvv;
while(!q.empty())
{
//计算每一层的元素大小
int sz =q.size();
//存储每一行的结果
vector<int> v;
for(int i =0;i<sz;++i)
{
Node* front = q.front();
q.pop();
v.push_back(front->val);
//vector<Node*> children;
//将下一层的数据全部入队列
for(Node* child: front->children)
{
q.push(child);
}
}
retvv.push_back(v);
}
return retvv;
}
};
这道题和上一道并没有什么新的知识点,我们直接转到下一道题
习题四: 腐烂的橘子
📝📝📝
994. 腐烂的橘子
题目:
这里我简单的画了一下每分钟分别是哪些橘子腐坏:
这道题目就有点意思了,他要求我们求出没有新鲜橘子为止所必须经过的最小分钟数,在这种求最小值的场景当中,BFS是很有优势的,它能够遍历所有可能返回最小值的结果,这题是可能有若干个腐烂的橘子,同一分钟它们都会腐烂他们的上下左右的橘子
注意:这题目不是很难,但是结合了之前所学的知识,标记矩阵不会的可以看看【算法入门】最短时间学会DFS深度优先搜索,以及队列初始化不是一定只入一个值,也并不是什么时候都是需要标记矩阵的,像这里的腐烂橘子本身就是一种标记,这道题目挺好的,值得动手写写!
int arr[4][2] ={ { 0, 1 }, { 1, 0 }, { 0, -1 }, { -1, 0 } };
class Solution {
public:
int orangesRotting(vector<vector<int>>& grid) {
if(grid.empty())
return 0;
//初始化队列:因为腐烂橘子可能不止一个,我们可以遍历这个二维数组先将所有腐烂的橘子入队列
queue<pair<int,int>> q;
int clock = 0;//记录时间
//记录是否有新鲜橘子
int flag =0;
int row =grid.size();
int col=grid[0].size();
for(int i=0;i<row;++i)
{
for(int j =0;j<col;++j)
{
//腐烂橘子为2
if(grid[i][j]==2)
q.push(make_pair(i,j));
if(grid[i][j]==1)
flag=1;
}
}
//这里1.如果没有数据入队列的话,就说明没有腐烂橘子,此时分两种情况,2.没有橘子和存在新鲜橘子
//1.存在新鲜橘子并且没法腐烂
if(q.empty() && flag==1)
return -1;
//2.没有新鲜橘子并且没有腐烂橘子
else if(q.empty()&& flag==0)
return 0;
//此时q当中存放着所有的腐烂橘子,他们在同一时间腐烂周围的橘子
//遍历队列当中所有可能
while(!q.empty())
{
//先前的题一开始sz都是1,这题就不一定是了
int sz =q.size();
//遍历队列当中的所有腐烂橘子 ,遍历一遍队列只过了一分钟!
for(int i =0;i<sz;++i)
{
pair<int,int> first = q.front();
q.pop();
//遍历队头元素他的周围的坐标,方向矩阵
for(int j =0;j<4;++j)
{
int newx =first.first+arr[j][0];
int newy =first.second+arr[j][1];
//判断坐标是否合法
if(newx<0||newx>=row||newy<0||newy>=col)
continue;
//如果是新鲜橘子,将新鲜橘子的变成腐烂橘子,并且入队
if(grid[newx][newy]==1)
{
//这里相当于对于新鲜橘子做了标记,就不需要标记矩阵了
grid[newx][newy] = 2;
q.push(make_pair(newx,newy));
}
}
}
clock++;
}
//走到这里,两种情况
//情况一:剩余新鲜橘子没有被感染
for(int i=0;i<row;++i)
{
for(int j =0;j<col;++j)
{
//若存在新鲜橘子,返回-1(题目要求)
if(grid[i][j]==1)
return -1;
}
}
//情况二:全部被感染,返回时间-1,因为最后一个新鲜橘子腐烂入队列后已经没有新鲜橘子需要感染了,但是clock也会++,所以这里要减一次!!这里也可以在前面判断,都一样。
return clock-1;
}
};
这道题之前,希望你不要轻易放弃,只要这题没问题,基本上算法入门了!
习题五: 单词接龙(较难)
📝📝📝
127. 单词接龙
这道题的难度虽然是困难,但是我们稍加分析,它只是我们之前几道题的结合。
分析:我们可以把beginWord先入队列,再将队列中的单词只做一个单词的变化查看是否能在dict(我所设置的哈希表)中查找到,如果可以,就入队列,每次将队列所有的都修改一次,并记录count(修改的次数),直到修改一个词后等于endWord我们就可以返回次数,如果队列遍历完,从队列中没有找到一个单词修改一个字母就能到达结果的,就表示没可能到这个词了。
这张图其实就能很好的表示每次只修改一个词,到最终与endWord相等。
图解:
5.1使用哈希表的原因
📝📝📝
注意:vector< string > & wordList,只能用< algorithm >这个库中的查找,效率是O(N),所以我们放入unordered_set< string > dict中查找,效率是O(1),所以我们会进行如下操作
class Solution {
public:
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
unordered_set<string> dict;
for(string& e: wordList)
{
dict.insert(e);
}
//因为修改单词的时候可能出现 dog -- cog,可能修改来修改去都在这两个走,所以已经修改成的单词我们都标记一下
unordered_map<string,int> visited;//默认visited当中的单词第二个参数都是0
//当最终endWord都不在wordlist当中,肯定找不到,返回0
if(dict.find(endWord) == dict.end())
return 0;
//这道题我们可以将beginWord入队列做初始化
queue<string> q;
q.push(beginWord);
visited[beginWord] = 1;
//记录修改次数,一开始也是算在里面的
int count=1;
//遍历队列当中的所有可能
while(!q.empty())
{
int sz =q.size();
//遍历队列中的每一个词
for(int i =0;i < sz;++i)
{
string s =q.front();
q.pop();
//s的长度
int len = s.size();
//对于队列中的每一个单词,我们转换单词每一个字符为其他25个字符,看看是否存在在dict字典当中
for(int j =0;j < len;++j)
{
for(int k = 'a';k <='z';++k)
{
//每次改变都要在s的基础上改变,所以我们要创建临时变量tmp
string tmp = s;
if(tmp[j] != k)
{
tmp[j] = k;
//变换一个单词之后检测是否能在dict中查找到
if(tmp == endWord)
{
//表示转换了之后就可以在dict当中找到了,只要是转换得来的都会在这个地方返回
return count+1;
}
if(dict.find(tmp) != dict.end()
&& visited[tmp] == 0)
{
cout<<tmp<<" ";
//如果修改的词在dict当中能查找到,就入队列
q.push(tmp);
//并且标记成1,以免下次进来又转换为这个单词
visited[tmp] =1;
}
}
else
continue;
}
}
}
count++;//遍历完队列当中的所有单词表示修改了一次
}
//转换得不到结果
return 0;
}
};
习题六:最小基因变化
📝📝📝
433. 最小基因变化
📝📝📝
思想是和之前差不多的,上题没问题的话,这题就是送分题
这里可以用个全局arr来存储我们要的ACGT,方便后续的代码使用
char arr[4]={'A','C','G','T'};
class Solution {
public:
int minMutation(string start, string end, vector<string>& bank) {
//库当中<algorithm>提供的查找:InputIterator find (InputIterator first, InputIterator last, const T& val);
//end不存在bank就不用再往下走了
if(find(bank.begin(),bank.end(),end) == bank.end())
return -1;
//记录次数
int count =0;
queue<string> q;
q.push(start);
unordered_set<string> genebank;
//这题和上题一样都需要哈希表标记,和哈希表查找
for(string& s: bank)
{
genebank.insert(s);
}
//标记
unordered_map<string,int> visited;
visited[start] = 1;
while(!q.empty())
{
int sz =q.size();
//每次将当前队列所有的元素遍历
for(int i =0;i<sz;++i)
{
//将一个单词进行修改,再和genebank基因库里的比较
string front = q.front();
q.pop();
//记录当前单词的长度
int len =front.size();
//对该单词的任一个字符进行修改
for(int j=0;j<len;++j)
{
string tmp =front;
for(int k =0;k<4;++k)
{
//进行一个字符的修改,这里用一个全局的arr用来保存修改字符的所有可能(4种)
if(tmp[j]!=arr[k])
tmp[j] = arr[k];
else
continue; // 表示修改过后和原单词相同,我们这里跳过就可以了
//修改后判断是否为我们最终单词
if(tmp == end)
return count+1;
//判断是否在基因库当中genebank
if(genebank.find(tmp) !=genebank.end() &&visited[tmp] == 0)
{
//入队列,做标记
q.push(tmp);
visited[tmp] =1;
}
}
}
}
count++;
}
return -1;//无法实现
}
};
习题七: 打开转盘锁
📝📝📝
752. 打开转盘锁
注意这道题每个数字都只能往上或者往下拨动一次,还有要处理当字符’(’0‘-1),(’9‘+1)这两种不正确的数字。
class Solution {
public:
int openLock(vector<string>& deadends, string target) {
//这道题目就是我们如果能够在不把deadends当中的string入队列就能进行从“0000” 到target的变换就输出次数
//不然就输出-1
//"0000"在死亡数字当中
if(find(deadends.begin(),deadends.end(),string("0000"))!=deadends.end())
return -1;
if(target == string("0000"))
return 0;
//用哈希set便于查找
unordered_set<string> deadword;
for(string& s: deadends)
{
deadword.insert(s);
}
unordered_map<string,int> visited;
//记录修改次数
int count =0;
//这题也需要标记矩阵避免来回挑拨数字
queue<string> q;
q.push(string("0000"));
//对初始进行标记
visited[q.front()] = 1;
while(!q.empty())
{
int sz =q.size();
//将队列中的每一个单词做一次改变
for(int i =0;i<sz;++i)
{
string front = q.front();
q.pop();
//记录当前单词的长度
int len =front.size();
//对一个单词进行调整
for(int j=0;j<len;++j)
{
string tmp =front;
//往上调整
int upNum = tmp[j]+1;
if(upNum == (10 +'0'))
upNum = '0';
//往下调整
int downNum = tmp[j]-1;
if(downNum == ('0'-1) )
downNum = '9';
//对tmp单词进行修改
//1.向上调整
tmp[j] = upNum;
if(target == tmp)
return count +1;
//死亡列表找不到并且之前没有入过队列
else if(deadword.find(tmp) == deadword.end() && visited[tmp] ==0 )
{
//在死亡列表中找不到,就入队列,做标记
q.push(tmp);
visited[tmp] =1;
}
//2.向下调整
tmp[j] = downNum;
if(target == tmp)
return count +1;
else if(deadword.find(tmp) == deadword.end() && visited[tmp] ==0 )
{
//在死亡列表中找不到,就入队列,做标记
q.push(tmp);
visited[tmp] =1;
}
}
}
count++;
}
//怎么修改都修改不到
return -1;
}
};
🔑 🔑 🔑
总结
这节讲述的BFS其实核心在一开始就已经说完了,对于求最小值的时候可以拿出来使用,但是习题是多样的,只有多练习才能够越来越理解他的思想,如果上述有什么表述不对的,欢迎来指点,
下节会讲回溯思想和著名的N皇后问题!让我们拭目以待。
如果可以的话,可以给一个小小的三连吗❤❤❤