基于优先队列的BFS——普适化版本BFS

基于优先队列的BFS——普适化的BFS

​ 前言:对于BFS的最少值的问题中,我们常常用BFS来解决每一步步长均一致的问题。但是,有些情况下,在每一轮搜索中,对于下一步的选择并非步长一定一致的。例如,在迷宫中到达某点的最短用时问题中,并非在每一个点消耗的时间都是一样的,有可能在某些特定点消耗的时间要比普通点的时间多,也可能少,这样我们如果我们用普通BFS,将其选入队列,我们就无法保证我们队列头一定是最优的(因为BFS解题,默认队列头的元素就是最优的,一旦队列头的元素满足,就可以直接结束),那么就有可能输出一个错误的结果,也就是说,常规BFS,对于题目的要求很高,一旦修改一些条件,便不能使用,普适程度不强。
​ 本讲,试图通过基于优先队列的BFS,将BFS普适化,让BFS的适用题境进一步扩大。

思想篇:高屋建瓴

优先队列与普通队列有什么区别?

优先队列=普通队列-先进先出+优先级队列排列

​ 优先队列(也称堆),与普通队列的本质区别,就在于优先队列,队列内部会自动按优先级排序,队首是优先级最高的元素,队尾是优先级最低的元素,而普通队列,则是按照插入队列的先后顺序,从头到尾排列。如我将3,5,2,1,4插入普通队列中,那么普通队列内部元素的顺序就也是3,5,2,1,4;而如果我把它们插入一个升序排序的优先队列,也就是小顶堆,那么它的结果将会是1,2,3,4,5。除了这个区别,其它的插入,删除,检验队列是否为空,求队列长度等一系列操作都是一样的,但是,操作中唯一有区别的一项,就是普通队列访问首元素是用front(),而优先队列访问首元素,是用top()(很好理解,优先队列讲优先级,而不是说前后的,top即是说优先级最高,与优先二字是对上的,因此在优先队列中我们用top()这一成员函数)。

部分BFS题目中,我们为什么要使用优先队列?

​ 我们先来回顾一下常规BFS是怎么进行一系列题目的求解的。
​ 假设题目的要求迷宫中从a点到b点的最短用时数,每一步消耗的时间都为1,那么我们BFS的思路,就是先求出第一步抵达的所有点,到这个点的用时都是1,看是否是b点,如果是b点就退出,输出结果1;再求出第二步所能抵达的所有点,到这个点的用时都是2,看是否是b点,如果是b点就退出,输出结果2,以此类推,直到再第n步搜到,输出结果n。队列内最前面的数据,就是最小的数据。

​ 但假设我这样改题目,在迷宫中,移动到行标列标之和为奇数的点,用时为1;移动到行标列标之和为偶数的点,用时为2,请问从a点到b点的最短用时是多少呢?如果还是按照普通bfs的思路,会不会出现问题呢?答案是肯定的。因为下一步的用时,有可能是1,也有可能是2,我们无法保证用时数就等于步数了!那么队列里最前面的数据,并不一定是最小的,那么求出来的答案是第一个搜到的,但并不一定是第一短时间到的(注意区别),因此它极有可能是个错误的答案。

​ 那么在这种情况下,用优先队列就可以解决这个问题了吗?显然是的。我们新建一个优先队列,以时间长短进行升序排列,那么,就算我先搜到了一个到达终点的路径,它也不一定会先输出,而是被排列在时间数据值比较小的未达终点的路径的后面。比如说,在搜索的第三轮,我找到了第一个用时为5抵达终点的路径结果,但此时还有许多用时为3、4但未抵达终点的路径结果,假设数据是这样的,3,3,4,4,5。5不会再拓展了,但前面4个小数,都还会再拓展,如果说某个数拓展到比5小的4结束了,那它会被放在优先队列的前面,一旦4位于优先队列最前列,会直接返回4,而不是像普通队列那样,先返回5。

优先队列中,最前列符合条件的元素是否就是答案?

​ 是的,当符合条件的元素,处于队列最前列的时候,可以直接返回该元素的值。为什么呢?因为在这个元素的后面,要么是大于等于它但是没搜完的元素,这些元素的值只会进一步加大,而不可能比最前面的符合条件的元素小;要么是大于等于它同时也搜完的元素,这些元素,本来就不是最优元素,也没必要输出。比如说,我搜出来5是满足题意的,后面还有6,7,7,8,但是后面可以不用管,直接就返回5。因为后面的值就两种情况,搜完的,没搜完的,都只可能再变大,在本来就没5大的基础上,更不可能比5大,因此5肯定是最优的。因此,当第一个符合条件的元素处于队列最前列的时候,它就是最优值,也就可以直接返回该元素的值。

怎么区分要用优先队列BFS,还是常规BFS?

​ 本质上来说,用优先队列BFS肯定也能做常规BFS能做的题目。例如,还是迷宫问题。每移动一步的用时都是1,我们用优先队列还是可以完成的。我们用时间升序的优先队列,答案还是一样的。**因为此时加入队列的数据,也是严格按照升序递增的,**如在第一步用时1,那么传入的就是1,1,1,1,第二步传入的可能就是2,2,2,2,2,2,2,第三步3,3,3,… ,3。

​ 因此,个人想法是,如果你确定是常规BFS能做,那就用常规BFS做,省时间。**但是,如果你不确定常规BFS能不能做,就不要纠结了,那就直接用优先队列BFS。**因为常规BFS能做的优先队列BFS也一定能做,常规BFS不能做的,优先队列BFS也可能能做,优先队列BFS不能做的,那也估计不是往BFS这个思路想了。

如何创建符合题意的优先队列?

​ 优先队列的类型,一般都用结构体类型。结构体的成员列表,应该包含两个量:一是可供状态转移的变量,还要包含记忆目标结果的变量。

​ 例如,在迷宫问题中,可供状态转移的量,就是坐标,因此我们要在结构体中定义横坐标和纵坐标,再者,还要包含记忆目标结果的变量,这里的目标结果是实时时间,因此我们还需要定义一个记录实时时间的成员变量。

代码篇:思出码随

创建优先队列の代码

创建格式如下:

priority_queue<队列元素类型>队列名;

例如,我想要一个整形的队列名为qu的优先队列:

priority_queue<int>qu;

再例如,我想要一个结构体pos(自定义的)类型的名为que的优先队列:

priority_queue<pos>que;

总体来说,创建优先队列,还是比较容易的。

定义优先队列的排列顺序の代码

​ 如果我们新建一个整形的优先队列,priority_queue<int>qu;,在默认情况下,也就是不给它任何规则的情况下,它是降序排列的,也就是“大顶堆”。

​ 如果想让一个整形的优先队列,升序排列,也就是成为一个“小顶堆”,那么我们需要将这句话改成,priority_queue<int,vector<int>,greater<int>>qu;,区别就是在int后面加个vector,再加个greater即可。

​ 但是,我们最常定义的是结构体类型优先队列的顺序priority_queue<pos>que;。结构体类型优先队列的顺序,定义就会稍显复杂一些。因此,我们再给出一个口诀,方便记忆其顺序定义。

struct T{
	int x,y,z;
	friend bool operator <(T t2,T t1){//fbo小于号,右前左后同sort 
		if(t1.z!=t2.z)return t1.z>t2.z;
		if(t1.y!=t2.y)return t1.y>t2.y;
		if(t1.x!=t2.x)return t1.x>t2.x;
	}
}t;

​ 结构体优先队列的顺序定义,直接在定义结构体语句==内部再定义==顺序。在结构体成员变量都定义完了之后,根据口诀fbo小于号,右前左后对着写,后面的顺序定义就和sort函数的结构体排序的操作全部一样的了。

​ 先解释一下什么是fbo小于号,f就是friend,b就是bool,o就是operator,小于号就是<,这必须是一个严格的从左到右的顺序,因此就是fbo小于号。然后就是右前左后,比如在sort函数顺序定义的时候,参数定义是左前右后,就比如bool cmp(int x,int y)然后return x<y,就是代表返回参数括号中左边的变量比右边的变量小,那么就是升序排列,但是在优先队列里面恰恰相反,如果friend bool operator <(int x,int y)然后return x<y,就是代表返回参数括号中右边的变量比左边的变量小,也就是降序排列。

​ 那么我们怎么防止混淆呢?就是通过定义参数时的右前左后,我们可以直接把x放在右边,y放在左边,也就是friend bool operator <int(int y,int x),return x<y,这样就全部都和sort函数一样了,与sort函数排序顺序定义相同的操作也就不用介绍了。

​ 这个口诀纯属作者自己脑补,方便记忆,如果能确保记住的,可以跳过这个口诀,哈哈。

基础操作の代码

/*优先队列的基础操作:以队列名为q的队列为例*/

/*1.入队*/ 			q.push(XXX);
/*2.出队*/ 			q.pop();
/*3.求队列中元素个数*/  q.size();
/*4.判断度列是否为空*/  q.empty();若为空返回true,否则返回false
/*5.返回q的第一个元素*/ q.top();//唯一的不同,在普通队列中是q.front();
/*6.返回q最后一个元素*/ q.back();

题目篇:相关典例

拯救丁爸——典型优先队列BFS迷宫问题

在这里插入图片描述

图1.优先队列BFS例题

在这里插入图片描述

图2.图1优先队列BFS例题的样例输入和样例输出

​ 本题由于警卫的存在,导致队列输入的数据非线性,普通BFS无法正确解题。因此,我们要用基于优先队列的BFS。题目要求最短时间,那么我们优先队列队首应该排列最短时间元素,因此我们将优先队列以时间为排列条件进行升序排列。这道题的队列类型怎么定义?正如刚才所说的,既要包括可供状态转移的量,也要包括记忆目标结果的量。前者就是坐标,后者则是时间,因此我们定义结构体变量,成员变量包含横坐标变量,纵坐标变量用以状态转移,时间则记录目标结果的量。

​ 这道题还有一个小技巧,那就是拯救丁爸的人可能有很多,但是丁爸只有一个,然而BFS最适合解决的是1到1或者1到多的模型,为了充分发挥BFS的优势,我们可以将思维逆转一下,改成丁爸去救任意一个人。结合上述代码篇的代码,创建优先队列并定义其顺序,接下来其它的步骤和迷宫题的BFS完全一致。AC代码如下

#include<bits/stdc++.h>
using namespace std;
int dir[4][2]={{1,0},{-1,0},{0,-1},{0,1}};
int vis[205][205],n,m,sx,sy;
char a[205][205];
struct pos{
    int x,y,cnt;
    /*定义结构体内部再定义优先队列排序规则*/
    friend bool operator <(pos t2,pos t1){//fbo小于号,右前左后同sort
        return t1.cnt<t2.cnt;//按照时间升序排列
    }
};
bool check(int x,int y){//检查是否没越界,是否不是墙,是否没访问过
    if(x>=1&&x<=n&&y>=1&&y<=m&&a[x][y]!='#'&&vis[x][y]==0)return true;
    return false;
}
int bfs(int st,int ed){
    memset(vis,0,sizeof(vis));//初首化访问数组为0
    priority_queue<pos> q;//创建结构体pos类型的优先队列q
    q.push({st,ed,0});//插入首元素,起点坐标及起点的时间状态
    vis[st][ed]=1;//表示首位置已经访问过
    while(!q.empty()){//队列非空
        pos cur=q.top();//将队列首元素存于cur变量中
        q.pop();//首元素出列
        if(a[cur.x][cur.y]=='r'){//如果是目标
            return cur.cnt;//直接返回当前的时间值,可以这样操作的原因已在思想篇解释过
        }
        for(int i=0;i<4;i++){//没找到就拓展:搜索下一步所有可能的状态
            pos nex={cur.x+dir[i][0],cur.y+dir[i][1],cur.cnt+1};//传入下一状态于nex变量
            if(check(nex.x,nex.y)){//如果通过检查
                if(a[nex.x][nex.y]=='x')nex.cnt+=1;//再看看是不是警卫点,如果是,时间再加1
                q.push(nex);//加入下一个元素
                vis[nex.x][nex.y]=1;//标记下一个元素的访问数组
            }
        }
    }
    return -1;//没有找到,返回-1
}
int main(){
    ios::sync_with_stdio(false);
    while(cin>>n>>m){
        for(int i=1;i<=n;i++)
            for(int j=1;j<=m;j++){
                cin>>a[i][j];
                if(a[i][j]=='a')sx=i,sy=j;//记录起点横纵坐标,以丁爸为起点
            }
        if(bfs(sx,sy)!=-1)cout<<bfs(sx,sy)<<endl;//如果搜索结果不是-1,输出结果
        else cout<<"Poor Dingba has to stay in the prison all his life."<<endl;
        //否则输出题目要求的失败语句
    }
    return 0;
}

参考

《2021杭电ACM-LCY算法培训入门班第12节课》

  • 13
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值