C++进阶课程第3期——宽度优先搜索(BFS)

大家好,我是清墨,欢迎收看《C++进阶课程——宽度优先搜索(BFS)》。

 啊,上一期我们的情况啊也是非常好的,今天直接开始!

例题——小老鼠走迷宫

我们先从例题开始。 

题目描述
一个M*N的迷宫矩阵由 0 和 1 组成,1 表示墙壁,0 表示通路。
一只小老鼠从左上角即坐标(0,0)出发,只能走上下左右四个方向(不能走斜线),问小老鼠能否吃到右下角出口即坐标(M-1,N-1)处的奶酪。

输入格式
第一行输入空格分开的两个整数,表示迷宫的行数和列数。
然后输入M行N列的迷宫矩阵。

数据范围
5 <= M,N <= 50

输出格式
若能走到出口,输出"yes",否则输出"no"

样例

输入数据 1

5 5
0 0 1 0 1
0 0 1 0 0
0 1 0 1 1
0 1 0 0 0
0 0 0 0 0

输出数据1

yes

输入数据 2

5 5
0 0 1 0 1
0 0 1 0 0
0 1 0 1 1
0 1 1 0 0
0 0 1 0 0

输出数据 2

no

讲解 

这种就是我们BFS算法里一个经典的小专题,叫做泛洪算法

说了那么多,先讲讲BFS。

BFS全称Breadth-First Search(广度优先搜索),是一种用于遍历或搜索树或图数据结构的算法。它从指定节点开始,逐层访问所有相邻节点,直到找到目标或遍历完整张图。

这道题看起来很复杂,可我们不妨把上面的地图做一些标记(我们拿样例1作为例子),我们让每一个格子都赋 1 个名称,用 A/B/C/D.....来标记(哈哈哈,幸好只有 25 个格子,所以用 1 个字母就可以标识了!)。然后,我们用绿色的格子来表示可以走到格子,用红色格子用来表示障碍格子。得到下图:

 1.刚开始从A点出发,可以到两个点。

   由题意得,从(0,0)出发,即A点。

   而如果从A点出发,从A往外探索,可以找到B、F两个点。

2. 上一轮新找到的点是 B 和 F,可以找到两个点。

    从 B 和 F 往外探索,各自找到 1 个儿子:G 和 K 。

    

我们假定找儿子的顺序是 右-下-左-上,所以 G 成了 B 的儿子,然后就不把 G 看作 F 的儿子了(重复计算没有任何好处)。如果改变查找的顺序,会得到略有不同的情况,但是算法的意思不变,最终的计算结果不变。

3.上一轮新找到的点是 G 和 K,从 G 和 K 往外探索。G 找到了 1 个儿子:P; K 没有儿子。

4.重复上述的搜索方法,最终搜索结果为:

所以,在第 8 步的时候找到了迷宫的出口 Y 。

如果我们把搜索到的点按照顺序连起来,会是下图这样:

上面这个按层拓展搜索的方法,就是 BFS 算法的思想了。我们可以把构建一个点的队列,不断的把新找到的点放在队列的尾部,不断的从队列的头部把点拿出来,继续搜索,从而把上面的搜索思想转变成程序。

程序主体

#define xx t.x + diff[k][0]
#define yy t.y + diff[k][1]
struct point{
    int x,y;
};
queue <point> q;//队列,革命的种子,存放着搜寻到的点
int diff[4][2] = {{0,1},{0,-1},{1,0},{-1,0}};
bool bfs(){
    vis[0][0] = true;
	q.push( {0,0} );
    //上面两行代码很重要
	point t;
	while(q.size())
    {
		t = q.front();
		q.pop();
		if(t.x==n-1&&t.y==m-1)
        {
			return true;
		}//找到了出口
		for(int k=0;k<4;k++)
        {
            //循环四次分别为四个方向
			if(xx<n&&xx>=0&&yy<m&&yy>=0&&!vis[xx][yy]&&v[xx][yy]==0)
            {
                //判断边界+判断是否经过过

                //把探索到的点都加入队列,打上标记
				q.push( {xx, yy} );
				vis[xx][yy] = true;
			}
		}
	}
    //遍历完还没找到
	return false;
}

练一练

这种简单的题你已经看过了,那我们增加难度,嘻嘻。 

桂城真题

题目描述

您正在玩一个游戏,给出一个地图,地图实际是一个 R 行 C 列的二维字符数组。'#' 表示障碍物,'.' 表示可通行格子,'v' 表示飞机。游戏中只有 1 架飞机,而且在第 1 行。

游戏有 3 个键:向左键、向右键、下降键。其中向左键可以把飞机向左移动一个格子,向右键可以把飞机向右移动一个格子,下降键可以把飞机向下移动一个格子。每按一次向左键需要消耗 1 个能量,每按一次向右键需要消耗 1 个能量,每按一次下降键需要消耗 0 个能量。

有一个特殊规定:对于地图的任意一行,飞机在该行所消耗的能量都不能超过 K。

你的任务是控制飞机降落,顺利到达第 R 行,不能碰到障碍物,而且消耗的总能量最少。

例如下图,R=7, C=9, K=2:

##..v..##
###.....#
#####...#
####...##
###..####
#.......#
#...#####

其中一种最优的方案是:

  1. 在第一行按“向右键”、“下降键”
  2. 在第二行按“下降键”
  3. 在第三行按“下降键”
  4. 在第四行按“向左键”、“下降键”
  5. 在第五行按“向左键”、“下降键”
  6. 在第六行按“下降键”
  7. 顺利到达最后一行。

总共消耗 3 个能量,已经是最优解了。

输入格式

第一行,3 个整数:R、C、K。 1≤R≤50, 3≤C≤50, 0≤K≤47。

接下来是 R 行 C 列的二维地图。

对于地图的每一行来说,开头都是连续的若干个障碍物格子,结尾也是连续的若干个障碍物格子,中间是连续的可通行格子(看题干描述的例子)。第一行比较特殊,因为有驾飞机,所以有一个可通行格子被 'v' 替代了。

输出格式

一个整数,顺利完成任务所需要消耗的最少能量。如果不能完成任务,输出 -1

分析

这道题,要怎样与宽度优先搜索融合呢?

在观察中思考。

此处搜索的方式有所不同,可以把它想象成一把刀。(如下图所示)

 

A点为飞机所在的地方,B点为A点向左或向右所达到的目标点,C点为B点的下一层的同列格。(sorry,这里图画得不好,线段BC画太长了)

而我们while循环里的for循环也该进行一定的修改:

①多加一层循环

有两个方向,分别为左和右,所以外层for循环循环次数为2次。

②第二层for循环循环次数为k次

每一行最多可左右移动k次

③添加能量数组

为了减少队列的元素,也为了减少没有用的方法,我们可以定义一个ans数组,用来存储到该层所用的最少能量。

④if语句的修改

随着循环次数的改变,以及数组的增加,还有障碍物的加入(由题意),我们应该多加几个if语句,首先要判断边界,其次判断此处是否有障碍物,最后判断所用的能量有没有减少。(只有减少了,本次搜索才有意义,我们为的是让能量使用尽可能少)

⑤其他

判断后,不仅要将该位置放进队列,还要把能量进行刷新。

for(int dir=0;dir<2;dir++)
{
    for(int i=0;i<=k;i++)
    {
        if(cc&&cc<=c&&t.r<r)
        {
            if(v[t.r][cc]=='.'||v[t.r][cc]=='v')
            {
                if(v[t.r+1][cc]=='.'&&ans[t.r+1][cc]>ans[t.r][t.c]+i)
                {
                    q.push({t.r+1,cc});
                    ans[t.r+1][cc]=ans[t.r][t.c]+i;
                }
            }
            else break;
         }
    }
}

大概就是这样了,其他小细节,比如说用字符数组存储之类的,可以自己完成。

代码实现 

#include<bits/stdc++.h>
using namespace std;
#define cc t.c+diff[dir]*i
int diff[2]={-1,1};
int ans[52][52],r,c,k;
string v[51];
struct point
{
    int r,c;
};
void bfs(int p)
{
    memset(ans,0x3f,sizeof(ans));
    queue<point>q;
    q.push({1,p});
    ans[1][p]=0;
    point t;
    while(!q.empty())
    {
        t=q.front();
        q.pop();
        for(int dir=0;dir<2;dir++)
        {
            for(int i=0;i<=k;i++)
            {
                if(cc&&cc<=c&&t.r<r)
                {
                    if(v[t.r][cc]=='.'||v[t.r][cc]=='v')
                    {
                        if(v[t.r+1][cc]=='.'&&ans[t.r+1][cc]>ans[t.r][t.c]+i)
                        {
                            q.push({t.r+1,cc});
                            ans[t.r+1][cc]=ans[t.r][t.c]+i;
                        }
                    }
                    else break;
                }
            }
        }
    }
}
int minn=0x3f3f3f;
int main(){
    cin>>r>>c>>k;
    for(int i=1;i<=r;i++)
    {
        cin>>v[i];
    }
    bfs(v[1].find('v'));//寻找初始时飞机的位置
    for(int i=1;i<=c;i++)
    {
        minn=min(minn,ans[r][i]);
    }
    if(minn!=0x3f3f3f)cout<<minn;//如果到达不了才会还是无穷大
    else cout<<-1;
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值