首页 > 其他分享 >「BFS」李白斗酒诗百篇

「BFS」李白斗酒诗百篇

时间:2022-12-27 18:23:35浏览次数:57  
标签:队列 双端 边权 BFS int 斗酒 && 百篇

本题为12月25日22寒假集训每日一题题解

题目来源:(未知)

题面

题目描述

李白斗酒诗百篇,长安市上酒家眠,天子呼来不上船,自称臣是酒中仙。

出自唐代杜甫的《饮中八仙歌》
知章骑马似乘船,眼花落井水底眠。
汝阳三斗始朝天,道逢麴车口流涎,恨不移封向酒泉。
左相日兴费万钱,饮如长鲸吸百川,衔杯乐圣称避贤。
宗之潇洒美少年,举觞白眼望青天,皎如玉树临风前。
苏晋长斋绣佛前,醉中往往爱逃禅。
李白斗酒诗百篇,长安市上酒家眠,(斗酒 一作:一斗)
天子呼来不上船,自称臣是酒中仙。
张旭三杯草圣传,脱帽露顶王公前,挥毫落纸如云烟。
焦遂五斗方卓然,高谈雄辩惊四筵。

且说李白喝了酒,吟完诗,准备回自己的房间睡觉。但已经喝醉酒的他不能转太多的弯。

给你一个N*N(2≤N≤100)的方格中,‘x’表示障碍,‘.’表示没有障碍(可以走),李白可以从一个格子走到他相邻的四个格子,但是不能走出这些格子。问李白从A点到B点最少需要转90度的弯几次。

输入

第一行一个整数:N,下面N 行,每行N 个字符,只出现字符:‘.’,‘x’,‘A’,‘B’;表示上面所说的矩阵格子,字符之间没有空格。

输出

一个整数:最少转弯次数。如果不能到达,输出"zui dao le"。

样例输入

3
.xA
...
Bx.

样例输出

2

提示

开始和结束时的方向任意。


思路分析

本题需要较多的思考,对思维要求比较大,希望各位阅读时多多思考.

本题要求的是转弯数的最小值,在图论中我们常常可求路径的最小值,所以我们尝试把转弯数定义为"路径长",那么本题题意可以转化为,当沿着同一方向前进时,边权为0,否者边权为1,且起点可以向任意方向,求起点到终点的最短路径.

转化后的此题类似于洛谷上第4554的升级版,我们可以使用同样的思路解决此问题.

第一种思路是直接使用最短路算法,不过要注意朝向会影响边权,所以需要额外对朝向进行记录和处理,感兴趣的可以自己尝试一下,这里就不详细展开说了(因为这类题用双端队列BFS其实更优).

第二种思路就是双端队列BFS.众所周知,BFS(广度优先搜索)是可以用来处理边权均相等的最短路问题的,此处虽然有两个权重,但是我们可以把这些边权为0的边所连接的点合起来看成是同一个点,把它们分别引出的边权为1的边看成是由这个合起来的点引出的一些边权为1的边,这样图上又只有边权为1的边了,就可以使用BFS了.

如何把边权为0的边所连接的点合起来看成是同一个点?这里要说到为什么BFS能够且只能处理边权均相等的最短路问题了.BFS可以看成是一批一批进行搜索的,在搜索当前批的时候同时把下一批的那些点放入了队列当中,当找到终点时只需要看当前是第几批的即可,因为每批之间差的路径长是相等的(边权相等).如果边权不相等,那么每批的路径长是不同的,所以这种方法就失效了.但是,如果存在权重为0的边,由于0边权相当于没有边权,可以看成是当前批的,所以我们就可以通过把边权为0的边所连接的这些点也加入当前批中,来使得BFS依旧适用,这和前面所说的把这些边权为0的边所连接的点合起来看成是同一个点是同一个意思.

普通BFS使用的是普通的队列,此处由于我们需要将新的元素添加到当前批中,因而需要使用双端队列,即头和尾都可以插入和删除的队列(此处不需要在尾部删除,只需要在头尾插入和头部删除即可).当我们搜到边权为0的边时,我们把它所连接的那个点放入队列的头部,即放入队列中当前批的头部(不可能添加到队列中当前批的末尾,因为末尾在队列中间的一个不确定的位置,是无法访问的),和当前批一起处理;而搜到正常边权的边时,正常放入尾部,在下一批中处理即可.这种BFS也称01BFS双端队列BFS,在除了0权边以外的边都是相等权重的图中,其效率可以优于使用 dijkstra.

这题可以直接使用双端队列BFS解决,不过同样的由于朝向会影响边权,也需要额外对朝向进行记录和处理(此处有个小坑,需要特殊处理路径交叉的情况,具体解决方法在下面有写),感兴趣的可以自己尝试一下,这里我们直接对此法继续优化(不会优化代码性能,如果你看不懂下面的内容,直接这么写其实就可以了).

由于我个人做的时候嫌记录朝向太麻烦了(不能用可爱的pair了),于是我就想,能否对此题中的双端队列BFS进行进一步的优化,让其不用记录方向呢?答案是在本题中是可以的.

根据前面的分析,在此题所使用的双端队列BFS中,我们每次都会把同方向的放入双端队列的头部,且显然方向是一维的,一次只会往头部放一个点,且由于是放入头部,所以这个放入的点会在当前点处理完后立刻被连续处理,然后往队列头部放入一下个同样也会被立即处理的同方向的点.由此可见,此处的双端队列放入头部的操作,其实是让我们统一且连续地处理这些方向相同的点,也就是在挨个遍历这些点.所以我们完全可以把放入头部的操作简单用一个循环来代替,即我们不把点放入头部,而是开启一个循环,在循环中逐个遍历这些同方向的点,遍历直到遇到已经入队的点(这样其实是有问题的,后面再谈)或者是边界、障碍为止,同时把每个点剩下三个方向连接的点入队尾即可.这样之后也不必用双端队列了,可以重新使用普通的队列.

不过这样依然是要存放方向的,因为在遍历之前我们需要知道第一个点的方向.那有没有办法可以不用存第一个点的方向捏?有的,那就是提前处理.我们在处理一个点的时候,直接向上下左右四个方向进行一次遍历,将这些点全部入队.即我们在让当前点四周的点入队的同时,提前把与其四周点同方向的点全部提前,在此时一并处理.而且,由于在之前也是这么操作的,所以和这个点同方向的点此时已经全部入队过了,被打上入队标记了(次数标记为已记录最小转弯数),是不会被再次启动遍历的(遍历会在开始时直接因为循环条件不满足中断),所以剩下的能被启动遍历的方向自然就是需要转弯的方向,直接将其转弯数设为当前加一即可.局部代码如下(省去):

    int dict[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; // 搜索方向
    typedef pair<int, int> Node; // (x,y)坐标对

    while (!que.empty())
    {
        Node now = que.front();
        que.pop();

        // 搜索该点周围的四个点,同时将与其四周点同方向的点全部提前在此时一并处理并入队
        for (int i = 0; i < 4; i++)
        {
            // 取出当前方向前进后点的坐标
            int x = now.first + dict[i][0];
            int y = now.second + dict[i][1];

            // g存放图,非负数表示到当前点的最小转弯数(即当前点已入队),-1表示障碍物(以及起点,后面会写到),-2表示当前点不是障碍物且还未处理,-3表示终点
            // 遍历此方向的点,直到越界、碰到障碍物或已入队,此处循环的条件是不完整的,后面会补充
            while (x >= 0 && x < n && y >= 0 && y < n && g[x][y] <= -2)
            {
                if (g[x][y] == -3) // 到达终点
                {
                    cout << g[now.first][now.second] + 1 << "\n";
                    return 0;
                }

                // 被遍历到的点一定是要转弯一次的,因为同一方向的在前一次的提前处理中,已经全部入队了
                g[x][y] = g[now.first][now.second] + 1;
                que.push(Node{x, y});

                // 同方向继续前进,提前在此时一并处理并入队
                x += dict[i][0];
                y += dict[i][1];
            }
        }
    }

如果你运气好,把到此为止的思路写好交上去,可能能够AC(我本人AC了,后来才发现其实是有漏洞的).不过此时其实还是存在一个问题.在同一批操作中,如果一条水平的路径和一条竖直的路径发生了交叉,由于不是同时进行处理的,先走过的路径会把交叉点标记成已处理,而后走过的路径会因为这一点已处理而结束循环.但实际上此处循环提前结束了,因为经过这个点之后还有很多同方向的点没有被加入队列中,导致答案出现错误.

此问题同样出现在直接使用双端队列BFS的方法中,本质原因是这个路径交叉点其实向四个方向的边权都是0,因而无论从哪个方向经过都不会使得"路径"变长,可以经过多次,并不像别的点只能走过一次.

这个问题的解决方法很简单,只需要特判这个路径交叉的点即可,即如果发现遍历到的这个点记录的转角次数正好比当前点大一(因为此处为了不记录方向,采用了提前处理,所以是大一;如果直接用双端队列BFS,那就是记录的转角次数与当前相等),我们依旧继续进行迭代.所以上面代码中while循环的条件应该写成:

x >= 0 && x < n && y >= 0 && y < n && (g[x][y] <= -2 || g[x][y] == g[now.first][now.second] + 1)

至此主要的问题就全部解决了,只需要再编写一段输入的代码来对输入进来的图进行编码即可.这里我以-3代表终点,-2代表路,-1代表障碍,非负数表示经过这个点的最小转弯次数.然后只需要从起点开始启动我们所编写的这段BFS即可.不过此处还有个小问题,由于题目规定起点的方向是任意的,所以起点周围点的转角次数应该都是0,这和我们写的这段BFS是不能直接兼容的(会标记成1),需要特别处理.但是,如果我们假定起点的转弯次数是-1,由于-1在加1后正好是0,此时便会变成我们想要的情况.这种方法即是所谓的把起点看成一个超级源点,常用来省去对起点的特判.同时-1也代表障碍物,可以用来标记起点已经入队过(其实我想的时候是反过来的,正是因此我才用-1代表障碍物),且从题目描述中显然可以看出起点和终点一定是不重合的,所以这样的写法是没有问题的.输入部分的局部代码如下:

    typedef pair<int, int> Node; // (x,y)坐标对

    int n;
    cin >> n;
    
    vector<vector<int>> g(n, vector<int>(n, -2)); // -2代表空
    queue<Node> que; // BFS所用队列
    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j < n; j++)
        {
            char c;
            cin >> c;
            if (c == 'A')
            {
                g[i][j] = -1; // 这里我把起点看成是一个超级源点,-1不仅可以代表这个点不能走,而且还能使得其周围的点在+1之后记录的转弯次数正好是0
                que.push(Node{i, j});
            }
            else if (c == 'B')
            {
                g[i][j] = -3; // 终点
            }
            else if (c == 'x')
            {
                g[i][j] = -1; // -1代表障碍物(都代表当前这个点不能经过)
            }
        }
    }

至此,本题解答完毕.

参考代码

#pragma GCC optimize(1)
#pragma GCC optimize(2)
#pragma GCC optimize(3, "Ofast", "inline")

#include <iostream>
#include <vector>
#include <queue>
using namespace std;


int main()
{
    ios::sync_with_stdio(false);
    int dict[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; // 搜索方向
    typedef pair<int, int> Node; // (x,y)坐标对

    int n;
    cin >> n;
    vector<vector<int>> g(n, vector<int>(n, -2)); // -2代表空
    queue<Node> que; // BFS所用队列
    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j < n; j++)
        {
            char c;
            cin >> c;
            if (c == 'A')
            {
                g[i][j] = -1; // 起点看成超级源点,下一个点在+1之后正好是转0次
                que.push(Node{i, j});
            }
            else if (c == 'B')
            {
                g[i][j] = -3; // 终点
            }
            else if (c == 'x')
            {
                g[i][j] = -1; // -1除了代表超级源点,也可以代表障碍物(都代表当前这个点不能经过)
            }
        }
    }

    // BFS(广度优先搜索)
    while (!que.empty())
    {
        Node now = que.front();
        que.pop();

        // 搜索该点周围的四个点,同时将与其四周点同方向的点全部提前在此时一并处理并入队
        for (int i = 0; i < 4; i++)
        {
            // 取出当前方向前进后点的坐标
            int x = now.first + dict[i][0];
            int y = now.second + dict[i][1];

            // 未越界且未走过,注意由于无法同时进行水平和竖直方向,会导致交叉时的路径被截断,所以在同批处理中的标记不应当视为障碍
            while (x >= 0 && x < n && y >= 0 && y < n && (g[x][y] <= -2 || g[x][y] == g[now.first][now.second] + 1))
            {
                if (g[x][y] == -3) // 到达终点
                {
                    cout << g[now.first][now.second] + 1 << "\n";
                    return 0;
                }

                if(g[x][y] != g[now.first][now.second] + 1)
                {
                    // 被遍历到的点一定是要转弯一次的,因为同一方向的在前一次的提前处理中,已经全部入队了
                    g[x][y] = g[now.first][now.second] + 1;
                    que.push(Node{x, y});
                }

                // 同方向继续前进,提前在此时一并处理并入队
                x += dict[i][0];
                y += dict[i][1];
            }
        }
    }

    cout << "zui dao le\n";
    
    return 0;
}

"正是我们每天反复做的事情,最终造就了我们,优秀不是一种行为,而是一种习惯" ---亚里士多德

标签:队列,双端,边权,BFS,int,斗酒,&&,百篇
From: https://www.cnblogs.com/geministar/p/zstu22_12_25.html

相关文章

  • BFS场景样例测试--最短路径--用Java实现
    我们今天研究下最短路径用BFS来实现 首先确定数据结构/***二叉树数据结构节点*/publicclassTreeNode{intvalue;TreeNodeleft;TreeNoder......
  • BFS场景样例测试--层次遍历--用Java实现
    我们今天研究下层次便利用BFS来实现 首先确定数据结构/***二叉树数据结构节点*/publicclassTreeNode{intvalue;TreeNodeleft;TreeNoderi......
  • 拆分自然数BFS
    任何一个大于1的自然数n(n<=10),总可以拆分成若干个小于n的自然数之和。当n=7共14种拆分方法:7=1+1+1+1+1+1+17=1+1+1+1+1+27=1+1+1+1+37=1+1+1+2+27=1+1+1+47=1+......
  • POJ 3278 Catch That Cow(BFS)
    POJ3278CatchThatCow​ 现在你在一个数轴上的位置x,位置k上有一头牛,你要抓住这头牛,也就是走到位置k上。那怎么走呢?你有两种走路的方法,一种是从x移动到\(2\tim......
  • P1379 八数码难题(BFS)
    P1379八数码难题​ 八数码问题就是在\(3\times3\)的棋盘上,摆有八个棋子,每个棋子上标有1至8的某一数字。棋盘中留有一个空格,我们用0来表示。给出一个初始局面,给......
  • BFS,DFS算法
    算法题目  基础:1.数组2.字符串3.排序4.贪心5.递归6.循环7.滑窗8.栈9.进制转换10.位运算11.队列12.哈希表13.链表14.线性表15.二分查找......
  • 搜索问题 DFS BFS
    搜索问题DFSBFSDFSdfs更多用于解决存在性问题和遍历性问题他可以很容易找到一个东西是否存在,比如说一条路径是否存在,不需要恢复现场也可以比较方便的展示所有的情况,......
  • 最简单的BFS走迷宫问题
    原题目链接:https://www.luogu.com.cn/problem/T279759?contestId=88579  题目背景人生辗转几十年,今天学长终于遇到了他的喜欢的女孩...题目描述在一个阴雨连绵的夜......
  • leetcode 139. 单词拆分(BFS 或者 DP)
    题目大意:给定一个非空字符串s和一个包含非空单词列表的字典wordDict,判定 s是否可以被空格拆分为一个或多个在字典中出现的单词。说明:拆分时可以重复使用字典中的单词。......
  • 我从写三百篇技术文章中学到的几件事 | 2022 年中总结
    时光不负,创作不停,本文正在参加​​2022年中总结征文大赛​​......