关于dfs(深搜)与bfs(广搜),在这里尝试用较为通俗的话来进行一下介绍与理解。
一、具体搜索顺序
我们以迷宫问题为例,先介绍其具体搜索方法。类似题目可参考洛谷P1605
迷宫问题:设有一迷宫有n行m列组成,每个单元格是空地或障碍,现要求一条从起点到终点的最短路径。
1、dfs
从起点开始,每到达一个空地,我们都可以按照一个固定的顺序向四个方向进行试探(如:右,下,左,上),如果下一个是空地且未被访问,就向前推进一格,同时将前一格标记为已访问。再对新的这格重复上述操作,直到一条路走到底(即:到达终点或死路)。若到达终点,得到总步数。由于此时路径是否最佳不确定,所以需要原路返回进行其他路径的试探,即回溯。注意回溯时需要将前一格设为未访问,否则无论其他什么路径都不会到达此处。
如红色路径所示,当达到终点后,进行回溯,如蓝色所示,回溯到(2,4)处时,向上又有一条新的岔路,则对此路径继续搜索,在这条路上一路到底后再进行回溯,回溯至(2,2)处时发现向下又有没去过的岔路,那就去!不断重复上述操作。
可以看出dfs总会把每条路径搜索个遍,这也就导致了其效率较低的特点。一般当数据范围为几十几百时使用还是挺不错的。
2、bfs
对于广搜,我们通常利用列队(queue,先进先出的数据结构)进行维护操作。具体算法为:
①:先将起点入队。
②:将与队首可拓展的(在此题中就是边相邻的)点按照一固定顺序(还是如:右,下,左,上)入队。若这个队首所有可能的拓展点全部已经拓展完了或者根本就没有可拓展的点,则将这个队首出队。重复上述②操作,直至到达终点或者队列为空结束。
到达终点即求出了最短路径长,队列为空则说明迷宫无解。
如图:
对起点拓展完成后,队列中剩余两个元素,再对队首点(1,2)进行拓展,(1,2)可拓展点只有(2,2),将其入队,再将(1,2)点出队。
不断重复此操作,即把队首可拓展的点都入队,再把这个拓展完了的队首给踢出去,最终:
二、具体代码实现
1、dfs
看上去有点长,实际上很清晰 ,具体意义看注释。
#include<iostream>
#define MAXN 10010
using namespace std;
int N,M;
int minn=2147483646;
int startx,starty;//起点
int endx,endy;//终点
int dx[4]={0,0,1,-1};
int dy[4]={1,-1,0,0};//四向移动
int maze[MAXN][MAXN];//记录迷宫空地和障碍,0表示障碍,1表示空地
int v[MAXN][MAXN];//记录访问情况,1表示已经访问
//dfs找迷宫最短路径
void dfs(int x,int y,int step)
{
if(x==endx&&y==endy)
{
if(step<minn)
{
minn=step;
}//结束条件写前面
}
for(int i=0;i<4;i++)//对四个方向搜索
{
if(maze[x+dx[i]][y+dy[i]]==1&&v[x+dx[i]][y+dy[i]]==0)//如果下一格是空地且未访问
{
v[x+dx[i]][y+dy[i]]=1;//先将下一步设置为已经访问,以方便进行下一行的递归
dfs(x+dx[i],y+dy[i],step+1); //递归搜索
v[x+dx[i]][y+dy[i]]=0;//回溯时务必把之前这一个设置为未访问
}
}
}
int main()
{
scanf("%d%d",&N,&M);
for(int i=1;i<=N;i++)
{
for(int j=1;j<=M;j++)
{
scanf("%d",&maze[i][j]);
}
}
scanf("%d%d%d%d",&startx,&starty,&endx,&endy);//输入
v[startx][starty]=1;//起点已经访问
dfs(startx,starty,0);
printf("%d",minn);
return 0;
}
2、bfs
需要使用queue列队,其每一个元素可用一个结构体存储。
#include<iostream>
#include<queue>
#define MAXN 10010
using namespace std;
int maze[MAXN][MAXN],v[MAXN][MAXN];
int N,M;
int startx,starty;
int endx,endy;
int dx[4]={0,0,1,-1};
int dy[4]={1,-1,0,0};///意义同上
int flag=0;
struct point
{
int x;
int y;
int step;
};//利用结构体存储每个点的坐标和步数
int main()
{
//输入N行M列的迷宫
scanf("%d%d",&N,&M);
for(int i=1;i<=N;i++)
{
for(int j=1;j<=M;j++)
{
scanf("%d",&maze[i][j]);
}
}
scanf("%d%d%d%d",&startx,&starty,&endx,&endy);//输入
queue<point> q;//申请队列
point start;//初始位置的坐标和距离
start.x=startx;
start.y=starty;
start.step =0;
q.push(start);//第一步:起点入队
v[startx][starty]=1;//起点已经访问
while(!q.empty())//第二步:不断循环搜索队首的拓展点,入队,再将队首出队
{
int x=q.front() .x;
int y=q.front() .y;
if(x==endx&&y==endy)
{
printf("%d",q.front() .step);
flag=1;
break;
}//结束条件写前面
for(int k=0;k<4;k++)
{
int tx,ty;//临时存储下一步坐标
tx=x+dx[k];
ty=y+dy[k];
if(maze[tx][ty]==1&&v[tx][ty]==0)//如果是空地且未访问,那这个点就可以拓展
{
point temp;//临时的拓展点用来存储数据入队
temp.x =tx;
temp.y=ty;
temp.step =q.front() .step+1;
q.push(temp);//把这个拓展点入队
v[tx][ty]=1;//这个点已经访问
}
}
q.pop() ;//拓展完将队首元素出队
}
if(flag==0) printf("No answer!\n");
return 0;
}
三、dfs,bfs相关习题
题目意思很简单,把马放在棋盘上,问它到达其他各个点所需的最小步数。解法就是个简单的bfs变形,由于要求每一个点的步数,即终点不唯一,故记录步数的变量可单独开一个数组。此外,搜索时也不是简单的四向移动,再注意一下输出格式就行了。(一开始一直把空格打在左边,WA了好久)
#include<iostream>
#include<queue>
#include<string.h>
#include<stdio.h>
using namespace std;
int n,m;
int startx,starty;
int v[500][500];//是否访问过
int step[500][500];//存储步数
int dx[8]={1,1,2,2,-1,-1,-2,-2};
int dy[8]={2,-2,1,-1,2,-2,1,-1};//"日"字形八向移动
struct point{
int x;
int y;
};//记录坐标,步数单独用数组存储
int main()
{
cin>>n>>m>>startx>>starty;
memset(step,-1,sizeof(step));
memset(v,0,sizeof(v));
queue<point> q;//申请列队
point start;
start.x = startx;
start.y = starty;
step[startx][starty]=0;
q.push(start);//bfs第一步:起点入队
v[startx][starty]=1; //起点已经访问
while(!q.empty() )//bfs第二步:不断拓展队首元素再出队
{
int x=q.front() .x;
int y=q.front() .y;//记录队首坐标
for(int i=0;i<8;i++)
{
int tx=x+dx[i];
int ty=y+dy[i];//临时存储可能拓展到的下一步坐标
if(tx<1||tx>n||ty<1||ty>m||v[tx][ty]) continue;//如果走出了棋盘或已经访问,直接考虑下一个可能的拓展点
point temp;
temp.x =tx;
temp.y=ty;
q.push(temp);//拓展得到的点入队
step[tx][ty]=step[x][y]+1;//记录步数
v[tx][ty]=1; //已经访问
}
q.pop() ;//队首出队
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
printf("%-5d",step[i][j]);//输出,注意格式
}
printf("\n");
}
}
题意是指和一个确定的字母相邻的有多少种不同的字母。本题可使用dfs进行搜索。这题dfs时,起点不唯一(上一题终点不唯一,都稍微变通一下就行了)代码如下:
#include<iostream>
using namespace std;
int n,m;
char room[110][110];
int desk[30]={0};//存储每一种颜色是否存在
char pre;//总统的桌子是什么颜色
int dx[4]={0,0,1,-1};
int dy[4]={1,-1,0,0};
void dfs(int x,int y)
{
for(int i=0;i<4;i++)
{
int tx=x+dx[i];
int ty=y+dy[i];
if(tx<1||tx>n||ty<1||ty>m) continue;//出界了,跳过
if(room[tx][ty]!='.')//如果有桌子
{
if(room[tx][ty]!=pre)//如果这个桌子不是总统的,那就是搜索目标了
{
desk[room[tx][ty]-'A']=1;//利用字母和A相减,同颜色的桌子就只对应desk数组中一个特定的位置,从而记录不同颜色的种类
room[tx][ty]='.';//搜索完成,标为已访问(相当于变成空地,用.做标记,其实你当然也可以额外开一个数组v记录)
}
else if(room[tx][ty]==pre)//这个桌子还是总统的
{
room[tx][ty]='.';
dfs(tx,ty);//注意!一定要先把这个位置标为已访问!
}
}
}
}
int main()
{
cin>>n>>m>>pre;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
cin>>room[i][j];
}
}//输入
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(room[i][j]==pre)
{
dfs(i,j);//注意不要写成dfs[i][j],作者老是这么干,找半天还找不到错误原因
}
}
}
int sum=0;
for(int i=0;i<30;i++)
{
if(desk[i]!=0) sum++;//统计标记过的数量
}
cout<<sum<<endl;
}
四、简单的小结一下:关于dfs与bfs,其模板并不复杂,但是由于其很强的灵活性,在具体题目中往往会存在较大改变。总之,还是要多做做,多感受感受方法与变化吧。