浅说广度优先搜索

前言

假设你现在身处一个迷宫之中,而你对这个迷宫完全不了解,你只知道其中一定有一个点是通往出口的。现在你有个天赋技能——无限分身,你以及你的分身可以在原地分裂出多个相同的人。问:你要如何做才能保证你能最快找到出口?

介绍

在搜索中,最重要的就是求解最短路,我们前面学习的求解最短路的办法无论怎么剪枝就会有重复路径,因为我们的方法都是尝试去走这条路,如果不行再返回,既然是尝试,那么肯定有错误路径。我们有没有办法直接让我们走的每一步路都一定是当前的最短路呢?
答案是有的,我们可以按层来搜索,从起点开始,搜索出所有第一层能到达的点,那么从起点到这些点的最短路就知道了,再从这些第一层能到达的所有点依次出发,那么能到达的所有点就是第二层的点,依次类推,那么对于任意一个点,第一次访问到的该点就是起点到该点的最短路径。

第一种办法就是用数组实现,像上面一样,每一层的点以此放入一个数组中,这样就需要我们开二维数组,第一维表示层数,但是我们不知道最多有多少层,也无法判断最多的一层有多少个点,所以开数组我们都需要开到最大,空间不够。
当然,我们发现只有相邻两层之间有关系,那么可以开滚动数组省空间。但是实际上我们发现,先被拓展的点,那么也先拓展其他点,典型的先进先出,所以开一个队列就搞定了。

思路、

(1)把起点放入队列,此时队列里面只有起点一个元素。
(2)遍历起点的所有方向,找出所有合理的并且没有被标记过的点放入队列中。
(3)当队头元素所有方向遍历完成后,队头元素出队,继续遍历当前队头元素的所有方向并且依次入队。
(4)重复执行上述操作,直到队列为空或者找到终点为止。如果队列为空,说明从起点到终点没有路径,如果找到终点,则当前路径长度就是最短路径长度

同样的,这里我们还是用数组来模拟队列(没学过的看我之前的文章)

#include <bits/stdc++.h>
using namespace std;

int head=1,tail=1;
int dr[4][2]={{0,1},{1,0},{-1,0},{0,-1}},used[1000][1000];

struct Q{
	int x,y;
	int step;
}q[1001000];

int mp[1000][1000];
int main() {
	int x,y,X,Y,n;
	cin>>n;
	for (int i=1;i<=n;i++){
		for (int j=1;j<=n;j++){
			cin>>mp[i][j];
		}
	}
	cin>>x>>y>>X>>Y;
	q[tail].x=x;
	q[tail].y=y;
	q[tail].step=0;
	tail++;
	used[x][y];
	while (head<tail){
		for (int k=0;k<4;k++){
			int tx=q[head].x+dr[k][0];
			int ty=q[head].y+dr[k][1];
			if (tx<1||tx>n||ty<1||ty>n)continue;
			if (mp[tx][ty]==0&&used[tx][ty]==0){
				if (tx==X&&ty==Y){
					cout<<q[head].step+1;
					return 0;
				}
				used[tx][ty]=1;
				q[tail].x=tx;
				q[tail].y=ty;
				q[tail].step=q[head].step+1;
				tail++;
			}
		}
		head++;
	}

	return 0;
}

深搜和广搜的对比

深搜和广搜各有优缺点,从效率上看,深搜的时间复杂度远大于广搜,那么我们为什么还要学习深搜呢?因为广搜是按层拓展,处于每一层的点到起点的距离被认为是相等的,所以每一层的路径长度都必须相等,否则就不可以用广搜,所以深搜的使
用范围比广搜大。
深搜的范围不仅仅不如,很多难题不会做的时候,都需要打暴力,但是很多暴力不知道循环的终点,每次循环的变化量都不一样,所以就需要用深搜来打暴力,称为爆搜,所以学好深搜,并且熟练剪枝是很有必要的。

既然,广度优先搜索的效率远高于深度优先搜索,那么我们为什么还要学习深搜呢?广搜能不能完全代替深搜呢?

我们以前学习的深搜的题包括以下几类:
一:非最短路(只能用DFS)
(1)起点到终点的所有路径
(2)起点到终点的所有方案
二:最短路
(1)每走一步花费的代价一样(DFS和BFS)
(2)每走一步花费的代价不一样(只能用DFS)

综上所述,因为BFS是按层搜索,所以找到的起点到终点的距离一定是最短路径,并且搜索过一次就不会再搜索第二次了。同样的,当权值不同的时候,层次就会发生错误,第一次求出来的未必就是最短路。

双端队列的广搜

在一个n*n的方格中,有两种颜色的格子,在相同颜色之间移动不需要消耗体力,但是在不同颜色之间移动需要消化1个单位的体力,问给定一个起点和一个终点,求起点到终点的最小体力消耗。n<=2000

这就是一道典型的权值为0和1的搜索题,如果用深搜,n<=2000,肯定超时。如果用普通广搜,搜到一个点就放入队尾,肯定是错误的,因为不满足队列中消耗的递增性。所以我们要用双端队列bfs。如果是相同颜色,无消耗,即和当前队首元素的优先级是一样的,所以我们完全可以把该点放入队首,这样不影响整个队列的有序性。如果是不同的颜色,消耗+1,和普通广搜一样,放入队尾,仍然可以保持原来的优先级。虽然这种思路很简单,但是在写法细节上和普通广搜还是有一些区别,一定要熟悉两种写法,以免混淆在一起。
有的时候会出现一种奇怪的题,虽然每条路径的长度不完全相同,但是也有规律性,比如路径长度只有0和k(常数),并且数据范围很大n<=1000,如果用深搜,时间过不了,如果用广搜,感觉按层拓展又不对。
实际上,当路径长度为0的时候就表示在当前这一层,我们可以很容易用循环来实现,但是实际上我们可以用双端队列来实现。

#include<bits/stdc++.h>
using namespace std;

struct node{
	int x,y;
	int step=0;
}st,ed,q[1000000];
int tail=10000,head=10000;//双端队列一定要留够空间
int mp[520][520],used[520][520],n;
int dr[4][2]={{0,1},{1,0},{-1,0},{0,-1}};

void bfs(node tmp){
	q[tail]=tmp;
	tail++;//手动模拟双端队列
	while(head<tail){
		tmp=q[head];
		if (tmp.x==ed.x&&tmp.y==ed.y){
			cout<<tmp.step;
			return;
		}
		head++;
		for (int i=0;i<4;i++){
			node T;
			T.x=tmp.x+dr[i][0];
			T.y=tmp.y+dr[i][1];
			if (T.x<=n&&T.x>=1&&T.y<=n&&T.y>=1&&used[T.x][T.y]==0){
				used[T.x][T.y]=1;
				if (mp[tmp.x][tmp.y]==mp[T.x][T.y]){//如果相同
					T.step=tmp.step;//则步数不变
					head--;//放入头
					q[head]=T;
				}else {
					T.step=tmp.step+1;//否则加1
					q[tail]=T;//放入尾
					tail++;
				}
			}
			
		}
	}
}
int main(){
	cin>>n;
	cin>>st.x>>st.y>>ed.x>>ed.y;
	for (int i=1;i<=n;i++){
		for (int j=1;j<=n;j++){
			cin>>mp[i][j];
		}
	}
	bfs(st);
	return 0;
}

广搜求方案数

广搜是没有重复路径的,当搜到终点的时候就会直接结束,但是很明显最短路径没有搜索完。当终点在第k层时,从第k-1层的某个点到达第k层的终点,此时搜索结束,但是还有第k-1层的其他点也可以到达终点,这些点的情况也需要考虑。所以在搜索的时候不能搜到终点就结束,而是先打一个标记,当搜索完终点所在这一层的所有点后结束才能保证所有到终点的路径都搜索到了,即当出队的点的路径长度等于起点到终点的最短路径时就可以结束了。
但是为了保证答案的正确性,我们需要记录从起点出发到任意一个点的最短路径的方案数,同时在统计答案时,累加上上一个点的方案数,而不是+1

#include<bits/stdc++.h>
using namespace std;
struct node{
	int x,y,step;
}q[510000],st,ed;
int n,m,k,head=1,tail=1;
int mp[1019][1010],used[1010][1010],num[1010][1010],dis[1010][1010];
int dr[4][2]={{0,1},{1,0},{-1,0},{0,-1}};
void bfs(node Point){
    q[tail]=Point;
    tail++;
    used[Point.x][Point.y]=1;
    num[Point.x][Point.y]=1;
    dis[Point.x][Point.y]=0;
    int flag=0;
    while (head<tail){
        Point=q[head];
        head++;
        for (int i=0;i<4;i++){
            node point;
            point.x=Point.x+dr[i][0];
            point.y=Point.y+dr[i][1];
            if (point.x>n||point.x<1||point.y>m||point.y<1)continue;
            if (mp[point.x][point.y]==0){
                if (used[point.x][point.y]==0){
                    if (point.x==ed.x&&point.y==ed.y){
                        flag=1;
                    }
                    used[point.x][point.y]=1;
                    q[tail]=point;
                    dis[point.x][point.y]=dis[Point.x][Point.y]+1;
                    num[point.x][point.y]=num[Point.x][Point.y];
                    tail++;
                }else if (used[point.x][point.y]==1&&dis[point.x][point.y]==dis[Point.x][Point.y]+1){
                    num[point.x][point.y]+=num[Point.x][Point.y];
                }
            }
        }
        if (flag==1&&dis[Point.x][Point.y]==dis[ed.x][ed.y]){
            cout<<dis[ed.x][ed.y]<<endl;
            cout<<num[ed.x][ed.y];
            return;
        }
    }
    cout<<"No Solution!";
}
int main(){
    int t1,t2;
	cin>>n>>m>>k;
    for (int i=0;i<k;i++){
        cin>>t1>>t2;
        mp[t1][t2]=1;
    }
	cin>>st.x>>st.y>>ed.x>>ed.y;
    if (st.x==ed.x&&st.y==ed.y){
        cout<<0;
        return 0;
    }
    bfs(st);
	return 0;
}

广搜求具体方案

广搜求具体方案和深搜求具体方案的方法类似,A点能够到达B点,那么B点的前驱就是A点,因为广搜每次走的都是最短路径,所以前驱一旦记录就不会发生改变。对于每一个点,单独开一个二维数组记录其前驱结点的行、列坐标,最后再从终点出
发一直找前驱直到找到起点为止。

#include<bits/stdc++.h>
using namespace std;

struct node{
	int x,y,step;
}st,ed,q[520000],qian[1000][1000];
int tail=1,head=1,cnt;
int n,m;
int mp[1000][1000],used[1000][1000],dr[4][2]={{0,1},{1,0},{0,-1},{-1,0}};
int l[300000],r[300000];
void bfs(node t){
	q[tail]=t;
	tail++;
	while(head<tail){
		t=q[head];
		head++;
		for (int i=0;i<4;i++){
			node point;
			point.x=t.x+dr[i][0];
			point.y=t.y+dr[i][1];
			if (point.x==ed.x&&point.y==ed.y){
				qian[ed.x][ed.y]=t;
				cnt=t.step+1;
				return;
			}
			if (point.x>n||point.x<1||point.y>m||point.y<1){
				continue;
			}
			if (used[point.x][point.y]==0&&mp[point.x][point.y]==0){
				qian[point.x][point.y]=t;
				used[point.x][point.y]=1;
				point.step=t.step+1;
				q[tail]=point;
				tail++;
			}
		}
	}
}
int main(){
	cin>>n>>m;
	for (int i=1;i<=n;i++){
		for (int j=1;j<=m;j++){
			cin>>mp[i][j];
		}
	}
	cin>>st.x>>st.y>>ed.x>>ed.y;
	bfs(st);
	if (cnt==0){
		cout<<"No Solution!";
		return 0;
	}
	int lx=ed.x,ly=ed.y;
	l[0]=ed.x,r[0]=ed.y;
	cout<<cnt<<endl;
	for (int i=1;i<=cnt;i++){
		l[i]=qian[lx][ly].x,r[i]=qian[lx][ly].y;
		lx=l[i],ly=r[i];
	}
	for (int i=cnt;i>=0;i--){
		cout<<l[i]<<" "<<r[i]<<endl;
	}
	return 0;
}

  • 16
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值