首页 > 其他分享 >关于BFS

关于BFS

时间:2023-06-17 22:46:56浏览次数:46  
标签:bfs short ln int BFS 队列 关于

BFS

目录 Content

  • 概述
  • 问题思考与性质
  • 典型应用
  • 优化与扩展

Part 1 概述

I.什么是BFS?

广度优先搜索(breadth first search),是以同层可达状态优先,一层层向外扩展的搜索算法。一般以队列实现

II.算法基本结构

基本结构

图源:CSDN @sigd

III.动画过程演示

通常,bfs会用在遍历图,下面的图生动地解释了bfs的过程。
bfs

图源:modb.pro @石衫的架构笔记

如图,bfs从初态(树的根节点)一直遍历完整棵树。
算法的过程基本如下:

  1. 先扩展根节点
  2. 再依次次扩展与他直接相连的节点
  3. 重复以上步骤

可以发现,第二步需要保证依次扩展下一层的所有点,为保证不重不漏,我们想到了利用一种数据结构:队列

FBI Warning:有关队列的详细介绍,请到这里食用

队列,是一种允许在队列尾部插入元素,从队列首取出的结构。因此我们称它为“先进先出”的数据结构(FIFO,first in first out).

示意图(OI-Wiki):
示意图

C++ STL 为我们提供了方便的队列结构,基础用法:

#include<queue>
using namespace std;
queue<数据类型> 名字;
名字.push(元素) ......插入元素
名字.pop()    .......弹出元素
名字.front()   ......返回队首元素
名字.empty() .........检查队列是否为空
名字.size()   .......元素个数
名字.clear()  ........清除队列

这是一点铺垫,剩余请听下回分解

Part 2 问题思考与性质

I.BFS与DFS的区别?

X 实现 适用场景 思想区别
dfs 递归 一步步确定的问题,有前后依赖性 执着往下走,这使得有时dfs可能会有很大损耗(例如:网络流FF)
bfs 队列 广泛用于图上,或者可以转换为图的问题 一层层考虑,在一般没有分层这个概念时不适用(例如:枚举全排列)
重要:其实 大多数(有例外,视情况而定) 问题dfs、bfs都可以互相转化,只是有一种更优

II.BFS有哪些性质?

  1. 在走地图等问题中,具体来说是每扩展一步代价固定的问题中,第一个bfs到的状态就是最优状态
  2. 单调性:bfs队列里的节点一定是按层数排序的
  3. 两段性:bfs队列里至多存在分属于两层的节点

III.BFS有哪些优点

一层层向下考虑,面对状态空间大、状态先后性分明的题目效率高、思考简单。

IV.使用时要注意什么细节

回顾刚刚的结构:

int/void bfs(初始状态){
	queue<int> q;
   q.push(初态);
   vis[初态]=true;//1.根据题目要求检查要不要标记(通常都需要)
   while(!q.empty()){
   int ln=q.front();q.pop();//2.记住取出状态不然必定死循环
   if(找到终点) return 代价;
   for(遍历下一层状态){
      if(判断是否合法){
      		vis[新的状态]=true;//3.标记别忘记
          q.push(新的状态);
      }
   }
   取消标记或回溯(可能不要)
  }
}

这一点最需要注意:
标记问题。

很多人(尤其学了SPFA之后)都搞不清楚到底要不要标记
在这里,可以这么理解:

就像dfs,bfs也有还原现场的步骤,由于要求的最短路不一定每一步代价都一样(不适用于性质1),所以需要回溯来放弃走过的点,以提供其他到达这个点的可能。

Part 3 典型应用

I.走地图问题

回家

小 H 在一个划分成了 \(n \times m\) 个方格的长方形封锁线上。 每次他能向上下左右四个方向移动一格(当然小 H 不可以静止不动), 但不能离开封锁线,否则就被打死了。 刚开始时他有满血 66 点,每移动一格他要消耗 11 点血量。一旦小 H 的血量降到 00, 他将死去。 他可以沿路通过拾取鼠标(什么鬼。。。)来补满血量。只要他走到有鼠标的格子,他不需要任何时间即可拾取。格子上的鼠标可以瞬间补满,所以每次经过这个格子都有鼠标。就算到了某个有鼠标的格子才死去, 他也不能通过拾取鼠标补满 HP。 即使在家门口死去, 他也不能算完成任务回到家中。

地图上有五种格子:

0:障碍物。

1:空地, 小 H 可以自由行走。

2:小 H 出发点, 也是一片空地。

3:小 H 的家。

4:有鼠标在上面的空地。

小 H 能否安全回家?如果能, 最短需要多长时间呢?

这道题非常具有代表性,而且还有一定思维含量。

首先想想,直接套用bfs模版,可以做吗?

不行。首先是血量限制,然后更复杂的是鼠标的限制。

血量限制只需要加一个状态就可以了(用struct)。这个好办

但鼠标为什么会有影响呢?可以看到,题目中说“鼠标是瞬间补充的”,也就是说当一个格子扩展过后不能直接否定它,有可能后面血量不够了还可以回来补充。或者对于一个非鼠标的格子,但是我们必须通过重复走它来拿到鼠标。
如下图所示:(来自题解区@BurningEnderDragon

1

如果我们不重复经过(4,1)(4,2)那么就无法拿到(4,3)的鼠标,此时我们无法到家。

所以重点就是我们如何选择过滤掉重复扩展。这道题中的重复扩展应该是在同一个血量下重复走一个格子。

#include<bits/stdc++.h>
using namespace std;
const int N=10;
int n,m,mp[N][N];
const int dx[4]={0,0,-1,1},dy[4]={-1,1,0,0};
bool vis[N][N][N];
struct node{
	int x,y,stp,bld;
};
int bfs(int x,int y){
	queue<node> q;
	q.push({x,y,0,6});
	vis[6][x][y]=1;
	while(!q.empty()){
		node ln=q.front();q.pop();
		if(ln.bld==0) continue;
		if(mp[ln.x][ln.y]==3){
			return ln.stp;
		}
		if(mp[ln.x][ln.y]==4){
			ln.bld=6;
		}
		for(int i=0;i<4;i++){
			int nx=ln.x+dx[i],ny=ln.y+dy[i];
			if(nx<1||nx>n||ny<1||ny>m||vis[ln.bld-1][nx][ny]||mp[nx][ny]==0) continue;
			vis[ln.bld-1][nx][ny]=1;
			q.push({nx,ny,ln.stp+1,ln.bld-1});
		}
	}
	return -1;
}
int main(){
	cin>>n>>m;
	int sx,sy;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			cin>>mp[i][j];
			if(mp[i][j]==2) sx=i,sy=j;
		}
	}
	int ret=bfs(sx,sy);
	cout<<ret;
}

我们通过给vis数组升维来表示血量。

II.洪水填充

例如:

填涂颜色

由数字 0 组成的方阵中,有一任意形状闭合圈,闭合圈由数字 1 构成,围圈时只走上下左右 4个方向。现要求把闭合圈内的所有空间都填写成 2。

可以发现,直接找规律非常困难(当时有人写这种做法,居然A了

我们可以从地图外开始遍历,就像洪水一样填充外圈,闭合的圈就像墙一样挡住了洪水。

III.最短路

<1.方格图最短路>
特殊的最短路,走一步的代价通常为1或定值

这样就可以利用BFS的性质快速解。

<2.SPFA>
借助BFS思想实现的简单实用的最短路算法。

void spfa(int s){
	queue<int> q;
	q.push(s);
	vis[s]=true;
	while(!q.empty()){
		int ln=q.front();q.pop();
		vis[ln]=false;
		for(int i=head[ln];i;i=e[i].next){
			if(dist[ln]+e[i].w<dist[e[i].v]){
				dist[e[i].v]=dist[ln]+e[i].w;
				if(!vis[e[i].v]){
					vis[e[i].v]=true;
					q.push(e[i].v);
				}
			}
		}
	}
	return;
}

由于要求最短路,所以此时vis数组的含义应该是“是否在队列中”而松弛操作是可以随时进行的,vis的意义仅仅是防止无意义的入队。

时间复杂度通常很快。但是遇见方格图、菊花图时效率会退化。如果是正权图建议用Dij-Heap,带负权就放心用spfa。(详细的以后讲

Part 4 优化与扩展

I.双端队列BFS

其实与普通BFS结构相差无几:

int/void bfs(初始状态){
	deque<int> q;
   q.push_back(初态);
   vis[初态]=true;//1.根据题目要求检查要不要标记(通常都需要)
   while(!q.empty()){
   int ln=q.front();q.pop_front();//2.记住取出状态不然必定死循环
   if(找到终点) return 代价;
   for(遍历下一层状态){
      if(判断是否合法){
      		vis[新的状态]=true;//3.标记别忘记
         if(权值==0)
          q.push_back(新的状态);
         else
          q.push_front(新的状态);
      }
   }
   取消标记或回溯(可能不要)
  }
}

其中,deque是双端队列,它可以在队列两头进行操作。
我们重点注意以下片段:

	if(权值==0)
          q.push_back(新的状态);
         else
          q.push_front(新的状态);

上面说过,bfs队列应该满足单调性,为什么我们在这里可以在队头插入呢?
其实是一种贪心的思想。分类讨论:如果要求最大权值

  1. 若新的点权值为0:那么它就没有权值为1的点好,于是可以把它放到后面
  2. 反之,放到前面提高效率。

这种方法适用于权值有2种的情况

II.优先队列BFS

同 I,只是deque换成了priority_queue(不了解的请OI-Wiki

III.双向BFS

可以同时使用2个队列,从终态初态一起开始搜索,直到他们产生交汇为止。
可以使得复杂度缩小一半。

典例 Nightmare II

给定一张 N×M 的地图,地图中有 1 个男孩,1 个女孩和 2 个鬼。

字符 . 表示道路,字符 X 表示墙,字符 M 表示男孩的位置,字符 G 表示女孩的位置,字符 Z 表示鬼的位置。

男孩每秒可以移动 3 个单位距离,女孩每秒可以移动 1 个单位距离,男孩和女孩只能朝上下左右四个方向移动。

每个鬼占据的区域每秒可以向四周扩张 2 个单位距离,并且无视墙的阻挡,也就是在第 k 秒后所有与鬼的曼哈顿距离不超过 2k 的位置都会被鬼占领。

注意: 每一秒鬼会先扩展,扩展完毕后男孩和女孩才可以移动。

求在不进入鬼的占领区的前提下,男孩和女孩能否会合,若能会合,求出最短会合时间。

笔记
设有点A(x,y),B(a,b)则A、B的曼哈顿距离为\(\lvert x-y \lvert +\lvert a-b\lvert\)

可以从男孩和女孩一起开始搜索。

P.S 因代码遗失,此处放的是@垫底抽风(Acwing)的题解:

#include <queue>
#include <cstdio>
#include <cstring>

using namespace std;

const short dx[4] = {0, 1, 0, -1}; // 四个方向
const short dy[4] = {1, 0, -1, 0};
const short N = 805;               // 地图大小上限

// 结构体存 男孩/女孩/鬼,(当然鬼也可以用pair)
// (x, y) 表示其坐标位置,s 表示到达该点的距离。
struct Point{short x, y, s;};
short n, m;     // 地图大小
bool g[N][N];   // 存地图。true 表示能走,false 表示不能走
short st[N][N]; // 存每个点的状态。没被到过是 0,被男孩到过是 1,被女孩到过是 2
Point ghost[2]; // 存两个鬼的坐标

// 返回 a - b 的绝对值(当然也可以用 abs 写)
short sub(short a, short b)
{
    return a > b ? a - b : b - a;
}

// 判断 (x, y) 在第 t 秒的时候是否被某个鬼占领
bool check(short x, short y, short t)
{
    // 枚举两个鬼,如果某个鬼到 (x, y) 的曼哈顿距离 < 2t 那么返回 false
    for (short i = 0; i < 2; i ++ )
        if (sub(ghost[i].x, x) + sub(ghost[i].y, y) <= t << 1)
            return false;
    return true; // 否则说明没有鬼占领,放回 true
}

// 扩展队列 q 中所有到初始点距离在 v * t 以下的点
// 其中 t 是时间,v 是速度
bool BFS(queue<Point> &q, short t, short v)
{
    // 只要 q 不空,并且队头元素到初始点的距离小于 v * t,就继续扩展
    while (q.size() && q.front().s < t * v)
    {
        Point u = q.front(); q.pop();      // 先取出队头
        if (!check(u.x, u.y, t)) continue; // 如果队头元素已经被鬼占领(好惨),则跳过
        for (short i = 0; i < 4; i ++ )    // 枚举四个方向进行扩展
        {
            short a = u.x + dx[i], b = u.y + dy[i]; // 求出按该方向移动一格后的坐标
            // 如果该坐标在地图内,且能走,且没被鬼占领
            if (~a && ~b && a < n && b < m && g[a][b] && check(a, b, t))
                if (!st[a][b]) // 如果该位置没被遍历过
                {
                    // 记录该位置被遍历过。
                    // st[u.x][u.y] 是被谁遍历的,则 st[a][b] 就是被谁遍历过的。
                    st[a][b] = st[u.x][u.y];
                    // 将该点加入队列中
                    q.push({a, b, u.s + 1});
                }
                // 如果遍历 (a, b) 的和遍历 (u.x, u.y) 的不是一个人,说明男女会合了,返回 true
                else if (st[a][b] != st[u.x][u.y]) return true;
        }
    }
    return false; // 男女没会合,返回 false
}

// 求出男女会合的最短时间。如果不能会合,返回 -1
short bfs(Point boy, Point girl)
{
    memset(st, 0, sizeof st); // 将 st 初始化为 0
    // q_boy 是男孩的 bfs 队列,q_girl 是女孩的 bfs 队列
    queue<Point> q_boy, q_girl;
    // 分别将男孩和女孩加入队列中
    q_boy.push(boy), q_girl.push(girl);
    // 男孩到的点标记为 1,女孩到的点标记为 2
    st[boy.x][boy.y] = 1, st[girl.x][girl.y] = 2;
    // 按时间从 1 开始枚举。只要有一个队列没空,就继续枚举。
    for (short t = 1; q_boy.size() || q_girl.size(); t ++ )
        if (BFS(q_boy, t, 3) || BFS(q_girl, t, 1)) // 男孩速度为 3,女孩速度为 1
            return t; // 如果在扩展男孩或者扩展女孩时扩展到了对方,那么直接返回 t
    return -1; // 跳出循环说明两个队列都空了,返回 -1
}

int main()
{
    short test;
    scanf("%hd", &test);
    while (test -- )     // 注意多组测试数据qwq
    {
        scanf("%hd%hd\n", &n, &m);
        Point boy, girl;
        // 读入地图。i 和 j 分别枚举行列,cnt 枚举当前读入到了第几个鬼
        for (short i = 0, cnt = 0; i < n; i ++ , getchar())
            for (short j = 0; j < m; j ++ )
            {
                char ch = getchar();
                if (ch == 'M') boy = {i, j, 0};       // 如果该字符为 'M',说明是男孩,存入 boy
                else if (ch == 'G') girl = {i, j, 0}; // 如果该字符为 'G',说明是女孩,存入 girl
                else if (ch == 'Z') ghost[cnt ++ ] = {i, j, 0}; // 该字符是 'Z',说明是鬼,存入 ghost
                g[i][j] = ch != 'X'; // 如果该字符不为 'X',则该位置能走
            }
        printf("%d\n", bfs(boy, girl)); // 输出 bfs 的结果即可
    }
    return 0;
}

//作者:垫底抽风
//链接:https://www.acwing.com/solution/content/16498/
//来源:AcWing
//著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

EOF


感谢观看。\(\huge{QwQ}\)

标签:bfs,short,ln,int,BFS,队列,关于
From: https://www.cnblogs.com/haozexu/p/17488417.html

相关文章

  • 关于KMP
    关于KMP平凡,而又不平凡的一天,12月31日,2022年的最后一天,让我们用几句代码迎接新年的到来。cout<<"Goodbye2022\n";printf("Hello2023!");扯正题。Kmp的简介KMP算法是字符串匹配算法,基础的用途是在文本串中快速查找与模式串相匹配的位置。一些感想我们在研究这个算法的......
  • 关于最小生成树
    关于最小生成树目录概述性质Prim算法实现例P1194买礼物Kruskal算法实现思想例P4047部落划分Part1概述一个连通图的生成树是一个极小的连通子图,它包含图中全部的n个顶点,但只有构成一棵树的n-1条边。我们定义无向连通图的最小生成树(MinimumSpannin......
  • 关于单调栈
    关于单调栈目录概述实现思想例一P5788[模版]单调栈例二P4147玉蟾宫Part1概述单调栈是一种其中元素具有单调性的线性结构。由于其栈的特性,这种结构可以用来处理左边\右边比它小\大的数。实际上,作者认为,遇到题目中元素:“限制”它的左、右且被其左、右“限制”的......
  • 痞子衡嵌入式:主流QuadSPI NOR Flash厂商关于QE位与IO功能复用关联设计
    大家好,我是痞子衡,是正经搞技术的痞子。今天痞子衡给大家讲的是几家主流QuadSPINORFlash厂商关于QE位与IO功能复用关联设计。痞子衡之前写过一篇文章《串行NORFlash下载/启动常见影响因素之QEbit》,这篇文章介绍了几家主流厂商关于QEbit在Flash内部寄存器位置以......
  • AMBA2 关于APB
    参考https://zhuanlan.zhihu.com/p/419750074https://zhuanlan.zhihu.com/p/623829190注:波形图片来自于AMBA2APBProtocolSPEC.1.APB的用处APB不支持流水线设计,不支持突发传输。APB和AHB一样,有数据总线和地址总线,读写使用PWRITE和HWRITE控制,不能同时读写数据。......
  • 1100. 抓住那头牛(bfs)
    https://www.acwing.com/problem/content/1102/数据范围为1e5实际上还可以再继续细分,加入特判来优化耗时,但是意义不大#include<iostream>#include<cstring>#include<cstdio>#include<queue>usingnamespacestd;constintN=1e5+10;intn,k;boolvis[N];int......
  • 关于Cookie Session 和Token,以及应用场景
    关于Cookie和Session(面试经常问)共同之处:cookie和session都是用来跟踪浏览器用户身份的会话方式。关于会话在日常生活中,从拨通电话到挂断电话之间的一连串的你问我答的过程就是一个会话。Web应用中的会话过程类似于生活中的打电话过程,它指的是一个客户端(浏览器)与Web服务器之间连......
  • 关于js单线程的问题
    为什么说js是单线程?为了搞清楚这个问题,我们需要先了解这几个问题:什么是线程?什么是进程?他们之间的关系?什么是任务队列(EventQueue),任务分类(宏任务、微任务)?什么是事件循环?为什么说js是单线程?为什么js要是单线程?接下来我们一起来看一下:什么是线程?什么是进程?他......
  • 关于vue2路由跳转问题记录
    1.vue路由间跳转和新开窗口的方式(query,params)路由间跳转配置:query方式:参数会在url中显示this.$router.push({path:'路由地址',query:{msg:'helloworld'}})params方式:传参数据不会在导航栏中显示,需要配合路由的name属性使用。this.$......
  • 关于安规标准中的过电压等级
    参照IEC(国际电工委员会)的标准:I级别是低压低能量级别,并带保护装置,一般指电子设备的内部电压;II级是低压高能量级别,从主供电电路分支出来的,家里照明电路220V电压属于此类;III级是指高压高能量级别,指固定安装的主供电电路,一般指380V三相电压 过电压定义:用数字表示的瞬态过电压条......