一道很经典的 BFS 题

想认真的写篇题解。

题目来自:https://www.luogu.org/problemnew/show/P1126

题目描述

机器人移动学会(RMI)现在正尝试用机器人搬运物品。机器人的形状是一个直径$1.6米的球。在试验阶段,机器人被用于在一个储藏室中搬运货物。储藏室是一个N×M的网格,有些格子为不可移动的障碍。机器人的中心总是在格点上,当然,机器人必须在最短的时间内把物品搬运到指定的地方。机器人接受的指令有:向前移动1步(Creep);向前移动2步(Walk);向前移动3步(Run);向左转(Left);向右转(Right)。每个指令所需要的时间为1秒。请你计算一下机器人完成任务所需的最少时间。

输入

第一行为两个正整数N,M(N,M≤50),下面N行是储藏室的构造,0表示无障碍,1表示有障碍,数字之间用一个空格隔开。接着一行有4个整数和1个大写字母,分别为起始点和目标点左上角网格的行与列,起始时的面对方向(东E,南S,西W,北N),数与数,数与字母之间均用一个空格隔开。终点的面向方向是任意的。

输出

一个整数,表示机器人完成任务所需的最少时间。如果无法到达,输出-1。

图例

分析

以前在刷刘汝佳的紫书《算法竞赛入门经典》时做过这道题,现在又再次遇到,幸不汝(辱)命,一次就过了。

BFS 性质

我们都知道,BFS 具有从起点到目标节点(或状态)路径最短的特性,但是使用 BSF 这一特性时需要注意,只有当所有的边权重相同(一般为1)时,它才具有此性质,边权不等时不具有。BFS 每次从一个节点只经历一次转移,当求单纯的求距离时可以认为每次转移的边权为一,转移次数最少的路径一定是距离最短的。我们可以用两张图来直观的表现这个特性:

在图上:

初始节点为 0,每次从一个节点向四周节点扩散,访问所有距离为一(相邻,经过一次状态转换)的节点。

第一次扩散访问了所有蓝色节点,并没有找到目标节点,继续扩散。第二次扩散访问了两个粉色节点,虚线节点并没有被访问。因为在扩散到左边的粉色节点时,我们已经找到了目标节点。那么不去搜索第三个粉色节点(虚线)节点不会丢解吗?在图中我们也确实能看到,虚线节点在经历一次扩散后,到达紫色节点,紫色节点再扩散一次,也可到达目标节点。不过,这个选择无论是从上述图片还是从我的描述文字来看,他的距离都不短。那么为什么会这样呢?我说一下我个人的理解。看下面一幅图:

在树上:

在树上的 BFS 被称为层次遍历。他的工作原理就如其名,每次访问一层的节点,同一层的节点有一个特点就是,他们的层数是相同的,也即到根节点的距离。当扩散到第三层时,(从左向右)第三个粉色节点作为目标节点被发现,此时我们可以对比三种情况:

  1. 由于第三个粉色节点为第一个目标节点,所以所有该节点左侧同层节点都不是目标节点,并且从这些节点继续扩散出的节点的层数(即到根节点的距离)一定 大于 第三个粉色节点到根节点的距离。
  2. 对于第三个粉色节点,也即我们的第一个目标节点(为什么强调 “第一个” 因为可能有不止一个目标节点,即多个解),由他扩散而出的子节点的距离一定 大于 这个节点。
  3. 对于虚线节点,由于他是不是目标节点,在未访问到时未知,而他的性质是和第一种情况相似的,所以去访问并扩散虚线节点我们能得到的结果是他的距离

    大于等于 第三个粉色节点

由此,可以得出如果只有一个解,那么第一次被发现的目标节点 即第三个粉色节点一定是距离根节点最近的。

在图上也可以类比层的概念,得出相同的结论。

解题思路

BFS 的性质讨论完,再来具体考虑这道题。这道题应该能明显看出来,是在图上寻找最短路的问题。不过首先需要对数据进行一些分析处理,才能更好的应用 BFS。此题唯一有些麻烦的就是,机器人具有半径,将每个格子单独处理不方便。由给出的图例可以发现,机器人一定会占据四个格子的空间,而一旦一个格子障碍物出现,那么这个障碍物格子所在的四个四方格上的中心格点机器人都不能走(机器人只走格点)。

所以在读完输入键一个图后,可以再创建一个简化版的图,mini map。它将原图中每四个格点当做一个点,而机器人只走这四个格子的中心,一旦一个四方格中有一个障碍,那么这个四方格认为不可走。这样一来机器人移动、机器人半径、单个方格障碍物问题就简化成了最常见的情况:在一个图上的点避开障碍物到达另一个点的最短路。(这中简化和机器人在原图上的移动是等价的,可以模拟一下)。

以下是这部分处理代码:

	for(int i = 0; i < n; ++i) {
for(int j = 0; j < m; ++j) {
cin >> graph[i][j];
}
} int p = 0,q = 0;
for(int i = 0; i < n-1; ++i) {
q = 0;
for(int j = 0; j < m-1; ++j) {
if(graph[i][j] || graph[i][j+1] || graph[i+1][j] || graph[i+1][j+1])
mini[p][q] = 1;//标记为障碍,否则为 0 表示可达
q++;
}
p++;
}

建出了图,现在考虑状态的转移。此题中,每个节点可以有三种转移情况,向左转,向右转,移动。

每种情况耗时为 1(可以认为是距离,权重)。关于转向问题,可以定义一个方向数组,按顺时针顺序给出北东南西,然后无论朝向那个方位,向左转就是数组索引减一,向右转就是加一,检索方向数组获得新的方位。完成一次转向后,就完成了一次状态转移,将新的节点入队列,这个新的节点在下一次被取出考虑进行转移状态时,他的当前方向就是移动的方向。

宏和数据结构:

#define MAX 50
#define DIR 4
#define N 0
#define E 1
#define S 2
#define W 3 struct Node {
int x,y;
int step = 0;
int d;
Node (int x,int y,int step,int dir) {
this->x = x,this->y = y,this->step = step,this->d = dir;
}
// Node () {}
// int pre = 0;
// int loc = 0;
}; int graph[MAX + 1][MAX + 1];
int mini[MAX + 1][MAX + 1]; //mini graph
int vis[MAX + 1][MAX + 1][DIR];
// N E S W
int dx[] = {-1,0, 1, 0};
int dy[] = {0, 1, 0,-1};
int n,m;

BFS 代码:


Node bfs(int sx,int sy,int tx,int ty,int dir) {
queue<Node> q;
Node start = Node(sx,sy,0,dir);
q.push(start);
// path[pi++] = start;
vis[sx][sy][dir] = 1;
while(!q.empty()) {
Node u = q.front();
q.pop(); if(u.x == tx && u.y == ty) return u; //尝试向左转
if(!vis[u.x][u.y][(u.d-1+DIR)%DIR]) {
vis[u.x][u.y][(u.d-1+DIR)%DIR] = 1;
Node nu = Node(u.x,u.y,u.step+1,(u.d-1+DIR)%DIR);
// nu.pre = u.loc; //记录路径信息
// nu.loc = pi;
// path[pi++] = nu;
q.push(nu);
}
//尝试向右转
if(!vis[u.x][u.y][(u.d+1)%DIR]) {
vis[u.x][u.y][(u.d+1)%DIR] = 1;
Node nu = Node(u.x,u.y,u.step+1,(u.d+1)%DIR);
// nu.pre = u.loc;
// nu.loc = pi;
// path[pi++] = nu;
q.push(nu);
}
//尝试移动 creep,walk,run
for(int i = 1; i <= 3; ++i) {
Node nu = Node(u.x,u.y,u.step+1,u.d);
//判断是是否有障碍物。只要有一个,就不必移动了。
bool isok = true;
for(int j = 0; j < i; ++j) {
if(mini[nu.x+dx[u.d]*j][nu.y+dy[u.d]*j])
{ isok = false; break; }
}
if(isok == false) break; nu.x += dx[u.d] * i;
nu.y += dy[u.d] * i; if(inrange(nu) && !vis[nu.x][nu.y][nu.d]) {
vis[nu.x][nu.y][nu.d] = 1;
// nu.pre = u.loc;
// nu.loc = pi;
// path[pi++] = nu;
q.push(nu);
}
}
}
return Node(-1,-1,-1,-1);
}

其中注释代码是用来记录最短路的路径信息。在这里是我用来测试程序的。

在调试的时候遇到一个 bug 花了我几个小时,机器人移动方式有 creep,walk,run,分别移动 1 、2 、3 步,在移动步数大于 1 时,不能只判断目标节点是否是障碍物,而要判断每一步是否有障碍物。

完整程序:

#include <iostream>
#include <algorithm>
#include <cstring>
#include <stdlib.h>
#include <memory.h>
#include <queue>
using namespace std; #define MAX 50
#define DIR 4
#define N 0
#define E 1
#define S 2
#define W 3 struct Node {
int x,y;
int step = 0;
int d;
Node (int x,int y,int step,int dir) {
this->x = x,this->y = y,this->step = step,this->d = dir;
}
// Node () {}
// int pre = 0;
// int loc = 0;
}; int graph[MAX + 1][MAX + 1];
int mini[MAX + 1][MAX + 1]; //mini graph
int vis[MAX + 1][MAX + 1][DIR];
// N E S W
int dx[] = {-1,0, 1, 0};
int dy[] = {0, 1, 0,-1};
int n,m; // Node path[MAX * MAX + 1];
// int pi = 0; bool inrange(Node nd) {
return nd.x >= 0 && nd.x <= n-2 && nd.y >= 0 && nd.y <= m-2;
} Node bfs(int sx,int sy,int tx,int ty,int dir) {
queue<Node> q;
Node start = Node(sx,sy,0,dir);
q.push(start);
// path[pi++] = start;
vis[sx][sy][dir] = 1;
while(!q.empty()) {
Node u = q.front();
q.pop(); if(u.x == tx && u.y == ty) return u; //尝试向左转
if(!vis[u.x][u.y][(u.d-1+DIR)%DIR]) {
vis[u.x][u.y][(u.d-1+DIR)%DIR] = 1;
Node nu = Node(u.x,u.y,u.step+1,(u.d-1+DIR)%DIR);
// nu.pre = u.loc; //记录路径信息
// nu.loc = pi;
// path[pi++] = nu;
q.push(nu);
}
//尝试向右转
if(!vis[u.x][u.y][(u.d+1)%DIR]) {
vis[u.x][u.y][(u.d+1)%DIR] = 1;
Node nu = Node(u.x,u.y,u.step+1,(u.d+1)%DIR);
// nu.pre = u.loc;
// nu.loc = pi;
// path[pi++] = nu;
q.push(nu);
}
//尝试移动 creep,walk,run
for(int i = 1; i <= 3; ++i) {
Node nu = Node(u.x,u.y,u.step+1,u.d);
////判断是是否有障碍物。只要有一个,就不必移动了。
bool isok = true;
for(int j = 1; j <= i; ++j) {
if(mini[nu.x+dx[u.d]*j][nu.y+dy[u.d]*j])
{ isok = false; break; }
}
if(isok == false) break; nu.x += dx[u.d] * i;
nu.y += dy[u.d] * i; if(inrange(nu) && !vis[nu.x][nu.y][nu.d]) {
vis[nu.x][nu.y][nu.d] = 1;
// nu.pre = u.loc;
// nu.loc = pi;
// path[pi++] = nu;
q.push(nu);
}
}
}
return Node(-1,-1,-1,-1);
} int main(int argc, char const *argv[])
{
freopen("/home/skipper/Documents/code/刷题/洛谷 OJ/重启/in.txt","r",stdin);
int sx,sy,tx,ty;
char dir;
cin >> n >> m;
for(int i = 0; i < n; ++i) {
for(int j = 0; j < m; ++j) {
cin >> graph[i][j];
}
} int p = 0,q = 0;
for(int i = 0; i < n-1; ++i) {
q = 0;
for(int j = 0; j < m-1; ++j) {
if(graph[i][j] || graph[i][j+1] || graph[i+1][j] || graph[i+1][j+1])
mini[p][q] = 1;
q++;
}
p++;
}
int d;
cin >> sx >> sy >> tx >> ty >> dir;
switch(dir) {
case 'S': d = S;break;
case 'N': d = N;break;
case 'E': d = E;break;
case 'W': d = W;break;
}; //test 查看 mini map
// for(int i = 0; i < p; ++i) {
// for(int j = 0; j < q; ++j) {
// cout << mini[i][j] << " ";
// }
// cout << endl;
// } Node res = bfs(sx-1,sy-1,tx-1,ty-1,d); //输出路径
// int pp = res.loc;
// do {
// cout << path[pp].x << "," << path[pp].y << endl;
// pp = path[pp].pre;
// }while(pp); cout << res.step << endl;
return 0;
}

如有错误,欢迎指正评论。

作者:Skipper

出处:https://www.cnblogs.com/backwords/p/10542486.html

本博客中未标明转载的文章归作者 Skipper 和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

一道很经典的 BFS 题的更多相关文章

  1. ZOJ2006 一道很尴尬的string操作题

    ZOJ2006(最小表示法) 题目大意:输出第一个字符串的最小字典序字串的下标! 然后我居然想试一试string的erase的能力,暴力一下,然后20msAC了,尴尬的数据.......... #in ...

  2. BZOJ4644: 经典傻逼题【线段树分治】【线性基】

    Description 这是一道经典傻逼题,对经典题很熟悉的人也不要激动,希望大家不要傻逼. 考虑一张N个点的带权无向图,点的编号为1到N. 对于图中的任意一个点集 (可以为空或者全集),所有恰好有一 ...

  3. UVA 674 Coin Change 换硬币 经典dp入门题

    题意:有1,5,10,25,50五种硬币,给出一个数字,问又几种凑钱的方式能凑出这个数. 经典的dp题...可以递推也可以记忆化搜索... 我个人比较喜欢记忆化搜索,递推不是很熟练. 记忆化搜索:很白 ...

  4. HDU 1372 Knight Moves(最简单也是最经典的bfs)

    传送门: http://acm.hdu.edu.cn/showproblem.php?pid=1372 Knight Moves Time Limit: 2000/1000 MS (Java/Othe ...

  5. HDU 1175 连连看(超级经典的bfs之一)

    传送门: http://acm.hdu.edu.cn/showproblem.php?pid=1175 连连看 Time Limit: 20000/10000 MS (Java/Others)     ...

  6. TTTTTTTTTTTT POJ 2112 奶牛与机器 多重二分匹配 跑最大流 建图很经典!!

    Optimal Milking Time Limit: 2000MS   Memory Limit: 30000K Total Submissions: 15682   Accepted: 5597 ...

  7. JAVA经典算法40题及解答

    JAVA经典算法40题 [程序1]   题目:古典问题:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第四个月后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少? 1.程序分 ...

  8. JAVA经典算法40题

    1: JAVA经典算法40题 2: [程序1] 题目:古典问题:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第四个月后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少? 3 ...

  9. JAVA经典算法40题(原题+分析)之分析

    JAVA经典算法40题(下) [程序1]   有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第四个月后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少?   1.程序分析:  ...

随机推荐

  1. 【BZOJ5507】[GXOI/GZOI2019]旧词(树链剖分,线段树)

    [BZOJ5507][GXOI/GZOI2019]旧词(树链剖分,线段树) 题面 BZOJ 洛谷 题解 如果\(k=1\)就是链并裸题了... 其实\(k>1\)发现还是可以用类似链并的思想,这 ...

  2. luogu4770 [NOI2018]你的名字 (SAM+主席树)

    对S建SAM,拿着T在上面跑 跑的时候不仅无法转移要跳parent,转移过去不在范围内也要跳parent(注意因为范围和长度有关,跳的时候应该把长度一点一点地缩) 这样就能得到对于T的每个前缀,它最长 ...

  3. Django练习——图书管理系统

    Django图书管理系统 创建一个项目 1. django-admin startproject 图书管理 2. cmd 命令终端下创建一个app python manage.py startapp ...

  4. java 11 ZGC(可伸缩,低延迟的gc)

    ZGC, A Scalable Low-Latency Garbage Collector(Experimental) 可伸缩,低延迟的gc ZGC, 这应该是JDK11最为瞩目的特性, 没有之一. ...

  5. 第十九节、基于传统图像处理的目标检测与识别(词袋模型BOW+SVM附代码)

    在上一节.我们已经介绍了使用HOG和SVM实现目标检测和识别,这一节我们将介绍使用词袋模型BOW和SVM实现目标检测和识别. 一 词袋介绍 词袋模型(Bag-Of-Word)的概念最初不是针对计算机视 ...

  6. codeforces-1138 (div2)

    想法题是硬伤,面对卡题和卡bug的情况应对能力太差 A.求两个前缀和以及两个后缀和,相邻最小值的最大值. #include<iostream> using namespace std; ; ...

  7. 金融量化分析【day112】:量化交易策略基本框架

    摘要 策略编写的基本框架及其实现 回测的含义及其实现 初步学习解决代码错误 周期循环的开始时间 自测与自学 通过前文对量化交易有了一个基本认识之后,我们开始学习做量化交易.毕竟就像学游泳,有些东西讲是 ...

  8. MySQL实战45讲学习笔记:日志系统(第二讲)

    一.重要的日志模块:redo log 1.通过酒店掌柜记账思路刨析redo log工作原理 2.InnoDB 的 redo log 是固定大小的 只要赊账记录在了粉板上或写了账本上,之后即使掌柜忘记了 ...

  9. VSCode CSS自动补充前缀

    1.安装AuotPrefixer. 2.代码里写css样式后,Ctrl+Shift+P,选择AutoPrefix CSS执行 结果如下

  10. [物理学与PDEs]第4章第2节 反应流体力学方程组 2.4 反应流体力学方程组的数学结构

    1.  粘性热传导反应流体力学方程组是拟线性对称双曲 - 抛物耦合组. 2.  理想反应流体力学方程组是一阶拟线性对称双曲组 (取 ${\bf u},p,S,Z$ 为未知函数). 3.  右端项具有间 ...