源自《啊哈算法》
目录
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<cstdio> //scanf(), printf()
struct note
{
int x; //横坐标
int y; //纵坐标
int f; //父坐标,本题不要求输出路径,不需要
int s; //步数
};
int main()
{
struct note que[2501]; //50*50地图,队列扩展不超2500
int 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; //上一步的基础上+1
tail++; //放最后
}
//若达目标点,停止扩展,退出循环
if(tx == p && ty == q) {
flag = 1;
break;
}
}
if(flag) break;
head++; //继续后续点的扩展
}
//tail指向队尾的下一位,所以减1
printf("%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<cstdio> //scanf(), printf()
struct note
{
int x; //横坐标
int y; //纵坐标
int f; //父坐标,本题不要求输出路径,不需要
int s; //步数
};
int main()
{
struct note que[2501]; //50*50地图,队列扩展不超2500
int 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; //上一步的基础上+1
tail++; //放最后
}
//若达目标点,停止扩展,退出循环
if(tx == p && ty == q) {
flag = 1;
break;
}
}
if(flag) break;
head++; //继续后续点的扩展
}
//tail指向队尾的下一位,所以减1
printf("%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<iostream>
#include<cstdio> //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地图,队列扩展小于401
int 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);
//更新Max
if(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<iostream>
#include<cstdio> //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主体在主函数外通过递归实现