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

本文深入探讨了基于优先队列的广度优先搜索(BFS)算法,详细讲解了优先队列与普通队列的区别、优先队列在BFS中的应用优势、创建优先队列的方法及其代码实现,并通过具体实例展示了优先队列BFS在解决复杂迷宫问题中的强大能力。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

基于优先队列的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节课》

<think>嗯,多源BFS,听起来像是一种BFS的变种。用户可能已经知道普通BFS是用来做单源最短路径的,那么多源的可能就是多个起点同时进行搜索。让我仔细想想,首先要确定多源BFS的定义用途。 首先,多源BFS应该是在一个图中,同时从多个源点开始进行广度优先搜索。这样的话,可能用于解决多个起点到其他节点的最短距离问题,或者找到最近的某个目标点的最短路径。比如,处理网格中的多个起点同时扩散的情况,像是计算每个点到最近起点的距离。 然后,我需要回忆一下普通BFS的过程。普通BFS是从一个起点开始,逐层扩展,记录每个节点的距离。而多源的话,可能初始时将所有源点都加入队列,并且它们的距离设为0。这样,每次处理队列中的节点时,普通BFS一样,但初始状态不同。这样处理的话,每个节点的最短距离就是离它最近的源点的距离。 接下来,应用场景可能有哪些呢?比如在网格问题中,多个火源同时蔓延,计算每个位置被火蔓延到的时间;或者在地图中,多个商店的位置,求每个住户到最近商店的距离。这些都是典型的多源最短路径问题,使用多源BFS可以有效解决,因为传统BFS每个源点都要跑一次的话时间复杂度会很高,而多源BFS只需要一次遍历,时间复杂度是O(N)或O(M*N)取决于问题规模。 然后需要讲清楚多源BFS的实现步骤。初始时,将所有源点加入队列,并标记它们的距离为0。之后,进行普通BFS步骤,处理每个节点的邻居,如果邻居未被访问过,就更新其距离并加入队列。这样,每个节点第一次被访问时的距离就是最短的,因为BFS的特性保证了层序递增,最近的源点最先到达。 举个例子可能会有帮助。比如,在一个二维网格中,0代表可走,1代表障碍物,多个起点。要计算每个0的位置到最近的起点的最短距离。这时候,多源BFS就能高效解决。将所有的起点坐标加入初始队列,然后进行BFS,每个节点记录离最近起点的步数。 需要注意的问题可能包括处理障碍物,或者如何处理已经被访问过的节点。同时,多源BFS的时间复杂度应该普通BFS一样,都是线性于节点数,因为每个节点只被访问一次。 此外,多源BFS的正确性基于广度优先搜索的最短路径性质,因为当多个源点同时开始扩散时,最先到达某个节点的那个源点的路径就是最短的。因此,不需要重复计算每个源点的情况,节省了时间。 可能还需要比较一下多源BFS分别对每个源点做BFS的时间复杂度。比如,如果有k个源点,每个BFS是O(N),那么总时间是O(kN)。而多源BFS只需要O(N),所以在k较大时,效率提升明显。 总结的话,多源BFS的核心思想是利用BFS的层次遍历特性,从多个源点同时出发,逐层扩展,从而在一次遍历中计算出所有节点到最近源点的最短距离。适用于多个起点需要同时计算最短路径的场景,能够有效降低时间复杂度。</think>多源BFS是一种基于广度优先搜索(BFS)的改进算法,用于**同时从多个起点出发,逐层扩展并计算最短路径**。以下是逐步介绍: --- ### **1. 核心思想** - **普通BFS**:从单个起点出发,按层遍历所有可达节点,记录每个节点的最短距离。 - **多源BFS**:初始时将多个起点同时加入队列,通过一次遍历即可计算所有节点到最近起点的最短距离。 --- ### **2. 适用场景** - 网格中计算每个位置到多个起点中最近一个的距离。 - 多个火源蔓延模拟(每个位置被火覆盖的最早时间)。 - 地图中多个商店定位问题(每个用户到最近商店的距离)。 --- ### **3. 实现步骤** 1. **初始队列**: - 将所有起点加入队列。 - 标记每个起点的最短距离为0,其他节点初始为无穷大(或未访问状态)。 2. **逐层扩展**: - 从队列中取出节点,遍历其相邻节点。 - 若相邻节点未被访问过,更新其最短距离为当前节点距离+1,并加入队列。 --- ### **4. 代码框架示例(网格问题)** ```python from collections import deque def multi_source_bfs(grid, sources): rows = len(grid) cols = len(grid[0]) dist = [[-1] * cols for _ in range(rows)] # 初始距离矩阵 q = deque() # 添加所有源点 for (x, y) in sources: dist[x][y] = 0 q.append((x, y)) # 方向:上、下、左、右 dirs = [(-1, 0), (1, 0), (0, -1), (0, 1)] while q: x, y = q.popleft() for dx, dy in dirs: nx, ny = x + dx, y + dy if 0 <= nx < rows and 0 <= ny < cols: if dist[nx][ny] == -1: # 未访问过 dist[nx][ny] = dist[x][y] + 1 q.append((nx, ny)) return dist ``` --- ### **5. 时间复杂度** - 与普通BFS相同,为$$O(N)$$(假设网格共有$$N$$个节点),远优于对每个起点单独运行BFS的$$O(kN)$$($$k$$为起点数量)。 --- ### **6. 关键点** - **队列初始**:所有起点需同时入队。 - **距离更新**:每个节点仅被第一次访问时更新为最短距离(BFS的天然特性)。 - **障碍处理**:在遍历相邻节点时需跳过障碍物(根据具体问题调整逻辑)。 --- ### **7. 示例分析** 假设网格如下(0为可通行,1为障碍),起点为$$(0,0)$$$$(2,2)$$: $$ \begin{matrix} 0 & 1 & 0 \\ 0 & 0 & 0 \\ 0 & 1 & 0 \\ \end{matrix} $$ 通过多源BFS得到的距离矩阵为: $$ \begin{matrix} 0 & -1 & 4 \\ 1 & 2 & 3 \\ 2 & -1 & 0 \\ \end{matrix} $$ --- ### **8. 总结** 多源BFS通过一次遍历即可高效解决多起点最短路径问题,适用于网格、图结构中的扩散类场景。其核心在于利用队列的先进先出特性,保证最短距离的正确性。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值