广度优先搜索的定义
搜索算法算是比较常用的一个基础算法,主要应用在图论中,最常见的是迷宫问题。今天本人又回顾了以前学习过的搜索算法并且在洛谷上刷了几道题,想着拿其中一道题来写个体会。
首先,广度优先搜索,顾名思义,尽可能多的向外扩散,比如我现在处在一个大棋盘中的某一个格子上,我就要把四周每个各自都探测一遍,然后在探测过的每一个格子的基础上继续向外扩散一圈,直到所有格子都被探测一边为止,我们完成了一个搜索。
那么怎么用代码实现这个算法呢?用递归或者迭代两种方式都是可以的,我比较习惯利用队列通过迭代的方式实现,同样递归实际上也是要用到栈,这里简单介绍一下C++STL中队列的用法吧,用C语言手撸队列容易出现bug且费时间。
C++队列
首先我们要包含一个文件头,队列的英文就是queue,所以非常好记
#include<queue>
queue <int> q;//中间<>内的就是要定义的队列的数据类型,q是变量名,可以自定义
int i=8;
q.push(i);//向队列中添加变量
q.push(8);//等价于上一行
q.pop();//从队列头取出一个变量,但是不会返回它的值
q.front()//返回队列第一个变量,但不会取出,所以这两个一般一起使用
q.empty()//用来判断一个队列是否为空,为空返回1
q.size()//返回队列的大小
上述包括常用的队列操作函数,大家可以稍微了解一下
如何利用队列实现广度优先搜索呢?下面给出一个迷宫题目来进行理解
有一个仅由数字 0 与 1 组成的 n×n 格迷宫。若你位于一格 0 上,那么你可以移动到相邻 4 格中的某一格 1 上,同样若你位于一格 1 上,那么你可以移动到相邻 4 格中的某一格 0 上。
你的任务是:对于给定的迷宫,询问从某一格开始能移动到多少个格子(包含自身)。
要完成这题就要从一个格子出发,不断的向外扩散,符合广度优先搜索的特性。
假定我们要从x,y这个坐标开始向外扩散,我们先把该坐标入队列,然后探测它的四周有没有符合题目要求的点,如果有判断一下是否访问过该点,如果没有就入队列,直到四周的四个点全部判断完,循环上述步骤,直到队列为空,每次遇到符合条件并且没有访问过的点就将结果数量+1,话不多说,下面上代码
#include<iostream>
#include<queue>
using namespace std;
#define N 1008
struct d{//定义一个结构体存储坐标点
int x;
int y;
};
char maze[N][N];//迷宫
queue<d> q;
int dir[4][2]={{-1,0},{1,0},{0,-1},{0,1}};//用来转换方向
int main()
{
d t;
int m,n;
cin>>n>>m;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
cin>>maze[i][j];
int x,y,sum;
while(m--)//n次查询
{
cin>>x>>y;
int result=1;
int vis[N][N]={0};//用来存储一个点是否被访问过
for(int i=0;i<=n;i++)
vis[0][i]=1,vis[i][0]=1;
t.x=x;
t.y=y;
q.push(t);//先将出发点入队列
vis[x][y]=1;
d p,dp;
while(!q.empty())
{
p=q.front();
q.pop();
for(int i=0;i<4;i++)//探测四周
{
int x2=p.x+dir[i][0],y2=p.y+dir[i][1];
if(x2>=1&&x2<=n&&y2>=1&&y2<=n)//未越界
if(maze[p.x][p.y]!=maze[x2][y2])//符合题目要求的点
{
dp.x=x2;
dp.y=y2;
if(!vis[p.x+dir[i][0]][p.y+dir[i][1]])//未访问过就入队列
{
q.push(dp);
vis[dp.x][dp.y]=1;
result++;//答案+1
}
}
}
}
cout<<result<<endl;
}
return 0;
}
面对这题,很快我就写出来以上代码,但是提交上后显示三个测试样例超时,我一看测试用例m最大取到10的五次方,如果每次查询都要重新向外探测确实会超时。这个时候就需要对代码进行优化了,我们从一个点向外扩散的前提条件是下一个扩散点满足他和当前点的值不相同,那么对符合条件的下一个点来说,上一个也是它的可以扩散的点,就像图论中的连通图,如果有一条可以走通的路线,那么这个路线上的任何一个点都能到达这个路线上的任何一个点,利用这个原理对这道题来说,从一个点出发后探测过的所有可以走通的点的答案是相同的,他们同属于一个连通图,这个连通图内的所有点的大难都是相同的!!
所以我们就不需要遇到一个查询点就重新探测一遍了,我们把用来记录是否被访问过的数组vis定义成全局的,再定义一个答案数组ans用来存储每个连通图的答案,用一个类似哈希的数组对一个访问过的坐标指定其连通图答案下表,如果后续有查询,我们直接利用下标在答案数组中以O(1)的时间复杂度就可以找到答案。干说理论可能有些抽象,下面是改进后的代码
#include<iostream>
#include<queue>
using namespace std;
#define N 1005
struct d
{
int x;
int y;
};
char maze[N][N];//用来存放迷宫
int ans[N*N],count=0,dr[N][N];//ans存储答案,dr用来存放答案下标
queue<d> q;//队列
int dir[4][2]= {{-1,0},{1,0},{0,-1},{0,1}};//用来向四周探查
int main()
{
d t;
int m,n;
cin>>n>>m;
for(int i=1; i<=n; i++)
for(int j=1; j<=n; j++)
cin>>maze[i][j];
int vis[N][N]= {0};
int x,y,sum;
while(m--)
{
cin>>x>>y;
if(vis[x][y]==1)
{
cout<<ans[dr[x][y]]<<endl;
continue;
}
int result=1;
t.x=x;
t.y=y;
q.push(t);
vis[x][y]=1;
dr[x][y]=count;
d p,dp;
while(!q.empty())//对一个点搜索直到尽头
{
p=q.front();
q.pop();
for(int i=0; i<4; i++)//探查四周
{
int x2=p.x+dir[i][0],y2=p.y+dir[i][1];
if(x2>=1&&x2<=n&&y2>=1&&y2<=n)//不越界
if(maze[p.x][p.y]!=maze[x2][y2])//可以移动
{
dp.x=x2;
dp.y=y2;
if(!vis[p.x+dir[i][0]][p.y+dir[i][1]])//未被访问过,入队列
{
q.push(dp);
vis[dp.x][dp.y]=1;
result++;
}
dr[x2][y2]=count;//标记答案下标
}
}
}
ans[count++]=result;
cout<<result<<endl;
}
return 0;
}
如有错误或者可以改进的地方,欢迎各位评论区留言