《啊哈算法第四章之bfs》(17张图解)
创始人
2024-05-16 03:39:14
0

源自《啊哈算法》 

目录

bfs正文

题目

思路 

完整代码1

完整代码2 

再解炸弹人

题目

思路

完整代码1

完整代码2

总结


bfs正文

第四章--深度优先搜索中,我们用dfs找到了寻找小哈的最短路

接下来,我们要用bfs(Breadth First Search)找到最短路径,并输出最少的步数

 

题目

1,有一天,我的女朋友一个人去玩迷宫,因为方向感很差,迷路了,我得知后马上去解救她

2,迷宫由m行n列的单元格组成(m和n都小于100),每个单元格要么是空地,要么是障碍

3,我的任务是帮助女朋友找到一条从迷宫起点通向女朋友位置的最短路径

4,注意障碍是不能走的,也不能走到迷宫外

思路 

本题中,bfs通过寻找一步以内(相对起点)可以到达的点,两步以内可以到达的点,三步以内....直到找到终点(目标点),或是把所有点都加入队列

 最开始小哼在(1,1)处,上一节dfs中我们的方法是,先让小哼往任意一个可行方向走,然后一直尝试下去,直到走不通就返回这里,这时深度优先,通过递归实现

本节我们通过“一层一层”扩展的方法找到小哈,扩展时每发现一个点,就将这个点加入到队列中,直至走到小哈的位置(p, q)为止 

最开始小哼在入口(1,1)处,一步之内(距离入口)可以到达的点有(1,2)和(2,1)

但小哈不在这两个点上,小哼只能通过(1,2)和(2,1)这两点继续往下走

当小哼走到(1,2),两步之内可以到达的点有(2,2);

当小哼走到(2,1), 两步之内可以到达的点有(2,2)和(3,1)

这时我们发现(2,2)这个点可以通过(1,2)到达,也可以通过(2,1)到达,此时需要一个数组标记某个点是否被访问过 

当然我们可以不需要,可以通过存储迷宫的二维数组直接操作,具体优化代码后面会有

同理,我们找到三步之内可以到达的点有(2,3),(3,2),(4,1)

但是依然没有到达小哈所在点,我们需要重复上述步骤,直到发现小哈

回顾刚才的算法,我们用结构体实现队列来模拟这个过程 

struct note
{int x; //横坐标int y; //纵坐标int s; //步数
};
struct note que[2501]; //地图大小不超过50*50,队列扩展在2500个以内
int head, tail;
int a[51][51] = {0}; //存储迷宫地图
int book[51][51] = {0}; //标记走过的点,防止被重复扩展//初始化队列
head = 1;
tail = 1;
//先将(1,1)加入队列,并标记
que[tail].x = 1;
que[tail].y = 1;
que[tail].s = 0;
tail++;
book[1][1] = 1;

然后从(1,1)开始,先尝试往右走到达了(1,2)

tx = que[head].x;
ty = que[head].y + 1; //tx, ty为临时变量

需要判断(1,2)是否越界

if(tx < 1 || ty < 1 || tx > m || ty > m)continue;

再判断(1,2)是否为障碍或是否走过

if(a[tx][ty] == 0 && book[tx][ty] == 0)

 如果满足上面的条件,将(1,2)入队,并标记该点已经走过

book[tx][ty] = 1;
//注意bfs每个点只入队一次,不需要将book数组取消标记//插入新的点到队列中
que[tail].x = tx;
que[tail].y = ty;
que[tail].s = que[head].s + 1; //步数是父亲步数 + 1
tail++;

 

 接下来尝试往其他方向走,从(1,1)也可以到达(2,1),所以将(2,1)也加入队列,代码实现与上述一样

 对(1,1)扩展完后,(1,1)就没用了,此时将(1,1)出队,出队操作如下

head++;

 接下来在新扩展出的(1,2)和(2,1)上往下探索,目前为止已扩展出从起点出发,一步以内可以到达的所有点,因为未到达女朋友的位置,所以继续

(1,1)出队后,队列中head指向(1,2),我们需要通过(1,2)继续扩展,通过(1,2)可到达(2,2),将(2,2)也加入队列 

(1,2)扩展完毕,也没用了,就将(1,2)出队. (1,2)出队后,head指向(2,1)这个点. 通过(2,1)可以到达(2,2)和(3,1),但因为(2,2)已经在队列中,因此只将(3,1)入队

截至目前,我们已经扩展出从起点出发两步以内可以到达的所有点,可依旧没到达女朋友的位置,因此继续重复上述步骤,知道遇到小哈,算法结束.

为了方便向四个方向扩展,我们需要一个next数组 

int next[4][2] = {{-1, 0},//上{1, 0}, //下{0, -1},//左{0, 1}};//右

完整代码1

#include //scanf(), printf()
struct note
{int x; //横坐标int y; //纵坐标int f; //父坐标,本题不要求输出路径,不需要int s; //步数
};
int main()
{struct note que[2501]; //50*50地图,队列扩展不超2500int a[51][51] = {0}, book[51][51] = {0};int next[4][2] = {{-1, 0}, //上{1, 0},  //下{0, -1}, //左{0, 1}}; //右int head, tail;int i, j, k, n, m, startx, starty, p, q, tx, ty, flag;scanf("%d %d", &n, &m);for(i = 1; i <= n; ++i)for(j = 1; j <= m; ++j)scanf("%d", &a[i][j]);scanf("%d %d %d %d", &startx, &starty, &p, &q);//队列初始化head = 1;tail = 1;//往队列插入迷宫入口坐标que[tail].x = startx;que[tail].y = starty;que[tail].s = 0;que[tail].f = 0;tail++;book[startx][starty] = 1;flag = 0; //标记是否到达目标点,1表示已到达//当队列不为空时while(head < tail) {//枚举四个方向for(k = 0; k <= 3; ++k) {tx = que[head].x + next[k][0];ty = que[head].y + next[k][1];//判断越界if(tx < 1 || ty < 1 || tx > n || ty > m)continue;//判断不为障碍且未走过if(a[tx][ty] == 0 && book[tx][ty] == 0) {//bfs每个点只入队一次book[tx][ty] = 1;//插入新的点到队列中que[tail].x = tx;que[tail].y = ty;que[tail].f = head; //本题不用que[tail].s = que[head].s + 1; //上一步的基础上+1tail++; //放最后}//若达目标点,停止扩展,退出循环if(tx == p && ty == q) {flag = 1;break;}}if(flag) break;head++; //继续后续点的扩展}//tail指向队尾的下一位,所以减1printf("%d", que[tail - 1].s);return 0;
}
5 4
0 0 1 0
0 0 0 0
0 0 1 0
0 1 0 0
0 0 0 1
1 1 4 3
7

下面是关于去掉book数组的优化,只需在原迷宫数组基础上,把去过的点赋值为-1就好

完整代码2 

#include //scanf(), printf()
struct note
{int x; //横坐标int y; //纵坐标int f; //父坐标,本题不要求输出路径,不需要int s; //步数
};
int main()
{struct note que[2501]; //50*50地图,队列扩展不超2500int a[51][51] = {0};int next[4][2] = {{-1, 0}, //上{1, 0},  //下{0, -1}, //左{0, 1}}; //右int head, tail;int i, j, k, n, m, startx, starty, p, q, tx, ty, flag;scanf("%d %d", &n, &m);for(i = 1; i <= n; ++i)for(j = 1; j <= m; ++j)scanf("%d", &a[i][j]);scanf("%d %d %d %d", &startx, &starty, &p, &q);//队列初始化head = 1;tail = 1;//往队列插入迷宫入口坐标que[tail].x = startx;que[tail].y = starty;que[tail].s = 0;que[tail].f = 0;tail++;a[startx][starty] = -1;flag = 0; //标记是否到达目标点,1表示已到达//当队列不为空时while(head < tail) {//枚举四个方向for(k = 0; k <= 3; ++k) {tx = que[head].x + next[k][0];ty = que[head].y + next[k][1];//判断越界if(tx < 1 || ty < 1 || tx > n || ty > m)continue;//判断不为障碍且未走过if(a[tx][ty] == 0) {//bfs每个点只入队一次a[tx][ty] = -1;//插入新的点到队列中que[tail].x = tx;que[tail].y = ty;que[tail].f = head; //本题不用que[tail].s = que[head].s + 1; //上一步的基础上+1tail++; //放最后}//若达目标点,停止扩展,退出循环if(tx == p && ty == q) {flag = 1;break;}}if(flag) break;head++; //继续后续点的扩展}//tail指向队尾的下一位,所以减1printf("%d", que[tail - 1].s);return 0;
}

输出是一样的,只是把book数组删去,在对应位置上补上a[tx][ty] = 1即可 

再解炸弹人

分别用bfs和dfs解决问题

题目

 还记得小霸王游戏机上的“炸弹人”吗,用放置炸弹的方法来消灭敌人

炸弹的爆炸方向沿上下左右四个方向

问在哪里放置炸弹可以消灭最多的敌人,已知两种墙,一种可以被炸掉

由于现在只有一枚炸弹,所以都用"#"表示(一枚炸弹可以炸掉这种墙,但也会被挡住)

敌人用"G"表示,空地用"."表示,只有空地才能放置炸弹

要求:统计炸弹放在哪个空地上,消灭的敌人最多(上下左右四个方向敌人个数的和)

输出消灭敌人最多的坐标,以及消灭的人数

思路

按照枚举的办法,炸弹放在(1,11)处,最多可消灭11个敌人,注意是从(0,0)开始计算,但小人无法走到(1,11),所以正确答案是放在(7,11)处,可消灭10个敌人

我们使用bfs或dfs(搜索)来枚举出所有小人可以到达的点,然后在这些点分别统计消灭的人数

 

( 先用bfs)

1,起点

小人从点(3,3)开始扩展,先将(3,3)入队,计算出该点可消灭的敌人数量

2,一步以内

通过(3,3)扩展出(2,3),(4,3),(3,2),(3,4),并将这些点入队,再分别计算每个点消灭人数

3,两步以内......

接下来,再通过(2,3)开始扩展......

4,直至所有点扩展完毕,bfs结束,最后输出扩展出的点里,消灭敌人最多的坐标和敌人数量

采用结构体实现队列,需要x, y记录坐标 

struct note
{int x, y; //横,纵坐标
};
struct note que[401]; //地图最大20*20,队列扩展最多400
int head, tail;
char a[20][20]; //存储地图

 初始化队列,设置为空(head == tail)

head = 1;
tail = 1;

 队列中插入起始坐标(startx, starty),并标记已在队列中 

que[tail].x = startx;
que[tail].y = starty;
tail++;
a[startx][starty] = -1; //标记已在队列中

 统计炸弹在该点消灭的敌人数量,方法与《啊哈算法》第三章一样《啊哈算法》第三章--枚举很暴力_码龄11天的博客-CSDN博客

此处将某点(i,j)放置炸弹消灭的人数,写成一个函数,函数名为getnum,方便后续调用

sum = getnum(i, j);

 还需要一个next数组便于往四个方向扩展

int next[4][2] = {{-1, 0}, //上{1, 0},  //下{0, -1}, //左{0, 1}}; //右

 接下来开始扩展,也是bfs的核心部分

while(head < tail) {//枚举四个方向for(k = 0; k < 4; ++k) {tx = que[head].x + next[k][0];ty = que[head].y + next[k][1];//判断越界if(tx < 0 || ty < 0 || tx > n - 1 || ty > m - 1)continue; //换另一个方向//未走过的空地if(a[tx][ty] == '.') {//每个点只入队一次a[tx][ty] = -1;//插入新扩展的点到队列中que[tail].x = tx;que[tail].y = ty;tail++;//统计新扩展的点,消灭敌人人数sum = getnum(tx, ty);//更新max值if(sum > max) {max = sum;mx = tx;my = ty;}}}head++; //一个点扩展结束后,head++才能对后续点扩展
}

以上就是bfs基本实现过程 

如果换dfs的话,从起点开始,每走到一个新点就统计该点消灭的人数,并从该点继续往后尝试,直到无路可走返回,再尝试其他没走过的方向,直到所有点都访问一遍

void dfs(int x, int y)
{int k, sum, tx, ty;//计算人数sum = getnum(x, y);//更新值和坐标if(sum > Max){ Max = sum; mx = x; my = y; }//方向数组int next[4][2] = {{-1, 0},{1, 0},{0, -1},{0, 1}};//枚举四个方向for(k = 0; k < 4; ++k) {//下一节点坐标tx = x + next[k][0];ty = y + next[k][1];//判断越界if(tx<0 || ty<0 || tx>n-1 || ty>m-1)continue;//未走过的空地if(a[tx][ty] == '.') {a[tx][ty] = 'B'; //标记走过dfs(tx, ty); //递归a[tx][ty] = '.'; //取消标记}}return; //可去掉
}

完整代码1

bfs解题

#include
#include //printf()
using namespace std;
struct note
{int x, y; //横纵坐标
};
char a[20][20]; //存储地图
int getnum(int i, int j) //统计四个方向的敌人
{int sum, x, y;sum = 0; //消灭敌人数x = i; y = j; //x,y临时变量while(a[x][y] != '#') {//不是墙if(a[x][y] == 'G') //是敌人sum++;x--; //向上}x = i; y = j;while(a[x][y] != '#') {//不是墙if(a[x][y] == 'G') //是敌人sum++;x++; //向下}x = i; y = j;while(a[x][y] != '#') {//不是墙if(a[x][y] == 'G') //是敌人sum++;y--; //向左}x = i; y = j;while(a[x][y] != '#') {//不是墙if(a[x][y] == 'G') //是敌人sum++;y++; //向右}return sum;
}
int main()
{struct note que[401]; //20*20地图,队列扩展小于401int head, tail;int i, j, k, sum, Max = 0, mx, my, n, m, startx, starty, tx, ty;//定义方向数组int next[4][2] = {{-1,0}, {1,0}, {0,-1},{0,1}};cin>>n>>m>>startx>>starty; //n行m列for(i = 0; i < n; ++i) cin>>a[i];//读入n行字符//队列初始化head = 1; tail = 1;//插入起始坐标que[tail].x = startx;que[tail].y = starty;tail++;a[startx][starty] = 'B'; //标记走过Max = getnum(startx, starty); //起点消灭敌人数mx = startx; my = starty;//当队列不为空while(head < tail) {//枚举四个方向for(k = 0; k < 4; ++k) {tx = que[head].x + next[k][0];ty = que[head].y + next[k][1];//判断越界if(tx<0 || ty<0 || tx>n-1 || ty > m-1)continue;//判断未走过的平地if(a[tx][ty] == '.') {//每个点只入队一次a[tx][ty] = 'B'; //标记走过//新扩展的点插入队列que[tail].x = tx;que[tail].y = ty;tail++;//统计可消灭人数sum = getnum(tx, ty);//更新Maxif(sum > Max) {Max = sum;mx = tx;my = ty;}}}head++; //继续后面点的扩展}printf("将炸弹放在(%d, %d)处,可以消灭%d个敌人", mx,my,Max);return 0;
}
13 13 3 3
#############
#GG.GGG#GGG.#
###.#G#G#G#G#
#.......#..G#
#G#.###.#G#G#
#GG.GGG.#.GG#
#G#.#G#.#.#.#
##G...G.....#
#G#.#G###.#G#
#...G#GGG.GG#
#G#.#G#G#.#G#
#GG.GGG#G.GG#
#############
将炸弹放在(7, 11)处,可以消灭10个敌人

完整代码2

dfs解题

#include
#include //scanf(),printf()
using namespace std;
char a[20][20]; //存储地图
int Max, mx, my, n, m; //声明为全局变量
int getnum(int i, int j) //统计四个方向的敌人
{int sum, x, y;sum = 0; //消灭敌人数x = i; y = j; //x,y临时变量while(a[x][y] != '#') {//不是墙if(a[x][y] == 'G') //是敌人sum++;x--; //向上}x = i; y = j;while(a[x][y] != '#') {//不是墙if(a[x][y] == 'G') //是敌人sum++;x++; //向下}x = i; y = j;while(a[x][y] != '#') {//不是墙if(a[x][y] == 'G') //是敌人sum++;y--; //向左}x = i; y = j;while(a[x][y] != '#') {//不是墙if(a[x][y] == 'G') //是敌人sum++;y++; //向右}return sum;
}
void dfs(int x, int y)
{int k, sum, tx, ty;//计算人数sum = getnum(x, y);//更新值和坐标if(sum > Max){ Max = sum; mx = x; my = y; }//方向数组int next[4][2] = {{-1, 0},{1, 0},{0, -1},{0, 1}};//枚举四个方向for(k = 0; k < 4; ++k) {//下一节点坐标tx = x + next[k][0];ty = y + next[k][1];//判断越界if(tx<0 || ty<0 || tx>n-1 || ty>m-1)continue;//未走过的空地if(a[tx][ty] == '.') {a[tx][ty] = 'B'; //标记走过dfs(tx, ty); //递归a[tx][ty] = '.'; //取消标记}}return; //可去掉
}
int main()
{int i, startx, starty;cin>>n>>m>>startx>>starty; //n行m列for(i = 0; i < n; ++i) cin>>a[i];//读入n行字符a[startx][starty] = 'B';Max = getnum(startx, starty);mx = startx;my = starty;dfs(startx, starty);printf("炸弹放在(%d, %d),最多消灭%d个敌人",mx,my,Max);return 0;
}

输出同上 

总结

bfs核心是扩展,dfs核心是递归

仔细对比,其实本题中,bfs和dfs代码

相同点

1,方向数组next[4][2]        2,getnum()函数得到的消灭人数

区别

bfs构造完getnum()后,直接进入主函数,通过队列和开头的while(head < tail)得到答案

dfs构造完getnum()后,还要构造dfs(),这才进入主函数,然后调用函数得到答案 

bfs的主体是在主函数里通过队列实现的,dfs主体在主函数外通过递归实现 

相关内容

热门资讯

安卓子系统windows11,... 你知道吗?最近科技圈可是炸开了锅,因为安卓子系统在Windows 11上的兼容性成了大家热议的话题。...
电脑里怎么下载安卓系统,电脑端... 你有没有想过,你的电脑里也能装上安卓系统呢?没错,就是那个让你手机不离手的安卓!今天,就让我来带你一...
索尼相机魔改安卓系统,魔改系统... 你知道吗?最近在摄影圈里掀起了一股热潮,那就是索尼相机魔改安卓系统。这可不是一般的改装,而是让这些专...
安卓系统哪家的最流畅,安卓系统... 你有没有想过,为什么你的手机有时候像蜗牛一样慢吞吞的,而别人的手机却能像风一样快?这背后,其实就是安...
安卓最新系统4.42,深度解析... 你有没有发现,你的安卓手机最近是不是有点儿不一样了?没错,就是那个一直在默默更新的安卓最新系统4.4...
android和安卓什么系统最... 你有没有想过,你的安卓手机到底是用的是什么系统呢?是不是有时候觉得手机卡顿,运行缓慢,其实跟这个系统...
平板装安卓xp系统好,探索复古... 你有没有想过,把安卓系统装到平板上,再配上XP系统,这会是怎样一番景象呢?想象一边享受着安卓的便捷,...
投影仪装安卓系统,开启智能投影... 你有没有想过,家里的老式投影仪也能焕发第二春呢?没错,就是那个曾经陪你熬夜看电影的“老伙计”,现在它...
安卓系统无线车载carplay... 你有没有想过,开车的时候也能享受到苹果设备的便利呢?没错,就是那个让你在日常生活中离不开的iOS系统...
谷歌安卓8系统包,系统包解析与... 你有没有发现,手机更新换代的速度简直就像坐上了火箭呢?这不,最近谷歌又发布了安卓8系统包,听说这个新...
微软平板下软件安卓系统,开启全... 你有没有想过,在微软平板上也能畅享安卓系统的乐趣呢?没错,这就是今天我要跟你分享的神奇故事。想象你手...
coloros是基于安卓系统吗... 你有没有想过,手机里的那个色彩斑斓的界面,背后其实有着一个有趣的故事呢?没错,我要说的就是Color...
安卓神盾系统应用市场,一站式智... 你有没有发现,手机里的安卓神盾系统应用市场最近可是火得一塌糊涂啊!这不,我就来给你好好扒一扒,看看这...
黑莓平板安卓系统升级,解锁无限... 亲爱的读者们,你是否还记得那个曾经风靡一时的黑莓手机?那个标志性的全键盘,那个独特的黑莓体验,如今它...
安卓文件系统采用华为,探索高效... 你知道吗?最近安卓系统在文件管理上可是有了大动作呢!华为这个科技巨头,竟然悄悄地给安卓文件系统来了个...
深度系统能用安卓app,探索智... 你知道吗?现在科技的发展真是让人惊叹不已!今天,我要给你揭秘一个超级酷炫的话题——深度系统能用安卓a...
安卓系统的分区类型,深度解析存... 你有没有发现,你的安卓手机里藏着不少秘密?没错,就是那些神秘的分区类型。今天,就让我带你一探究竟,揭...
安卓系统铠无法兑换,揭秘无法兑... 最近是不是有很多小伙伴在玩安卓系统的游戏,突然发现了一个让人头疼的问题——铠无法兑换!别急,今天就来...
汽车安卓系统崩溃怎么刷,一键刷... 亲爱的车主朋友们,你是否曾遇到过汽车安卓系统崩溃的尴尬时刻?手机系统崩溃还能重启,但汽车系统崩溃了,...
miui系统可以刷安卓p系统吗... 亲爱的手机控们,你是否对MIUI系统情有独钟,同时又对安卓P系统的新鲜功能垂涎欲滴?今天,就让我带你...