前文,https://blog.csdn.net/justidle/article/details/104631780,我们对 BFS 进行了概要性介绍。从这里我们可以知道 BFS 的实现通常都需要队列 queue(FIFO特性)这个数据结构支持。
BFS 基本框架
下面,我们给出 BFS 算法的基本框架:
1、使用合适的数据结构来描述问题。如描述一个迷宫。
2、定义一个队列,来保存下一步访问的节点。
3、定义一个合适的数据结构来描述已经访问的问题。可以使用队列,也可以使用数组(比如使用 vis 布尔数组来描述某个节点是否已经访问过)。
4、将入口节点加入到队列中。
5、根据题目的要求,从入口节点开始遍历所有节点,直到找到结果或者是遍历所有节点无法找结果。
BFS 伪码
queue<xxx> q;//q表示下一个访问节点队列
xxx head;head表示入口节点
记录head节点为已经访问;
q.push(head);
while (q.empty()) { //当队列q不为空,则继续遍历
xxx tmp = q.front(); //取出队列q的首数据
q.pop();
//判断 tmp 节点是否为终点
if (tmp为目标状态) {
输出记录,结束遍历
} else {
按照题目要求,生成下一步节点next
if (判断 next 的合法性) {
记录next节点为已经访问;
q.push(next);//将合法节点推入到队列中
}
}
}
BFS 代码核心
合适的数据结构来描述题目
个人比较喜欢定义一个结构体,在这个结构体里,将 vis、迷宫等相关的数据放在一起。
其实这样的写法,主要是为了代码的可读性。其他方面没有任何帮助。
定义一个访问队列
个人推荐 C++ 的 STL 中的 queue 这个类,这样可以减少代码量,而且不容易出错。当然也可以自己实现 FIFO 队列。
处理入口节点
开始的时候,一般都提供一个入口节点。我们将入口节点的数据处理好后,再将其推入到 queue 中即可。
终点判断
每次从 queue 中获取队首元素后,判断该位置是否是终点。如果是终点,BFS 结束;如果不是终点,按照题目要求走下一步。
移动规则和合法性判断
按照题目要求写出合法性判断函数。根据题目的要求,写好移动规则。以后每一步都是遍历所有移动规则,算出下一步坐标。再进行合法性判断,如果合法,则将次节点推入到队列中;如果不合法,则放弃此节点。
BFS 算法实现
可以使用递归,也可以使用递推。各有好处。
BFS 实例
下面我们用一个实际题目,来分析 BFS 的基本实现过程。
题目相关
题目描述
有一间长方形的房子,地上铺了红色、黑色两种颜色的正方形瓷砖。你站在其中一块黑色的瓷砖上,只能向相邻的黑色瓷砖移动。请写一个程序,计算你总共能够到达多少块黑色的瓷砖。
输入
第一行是两个整数 W 和 H,分别表示 x 方向和 y 方向瓷砖的数量。W 和 H 都不超过 20。
在接下来的 H 行中,每行包括 W 个字符。每个字符表示一块瓷砖的颜色,规则如下
1)'.':黑色的瓷砖;
2)'#':白色的瓷砖;
3)'@':黑色的瓷砖,并且你站在这块瓷砖上。该字符在每个数据集合中唯一出现一次。
输出
输出一行,显示你从初始位置出发能到达的瓷砖数(记数时包括初始位置的瓷砖)。
样例输入
6 9
....#.
.....#
......
......
......
......
......
#@...#
.#..#.
样例输出
45
题目分析
这是一个非常典型的使用 BFS 走迷宫。只不过本题不是找到出口,而是遍历所有能走到的瓷砖。所以和标准出口寻找的停止条件会有所不同。
题意分析
入口点:
用 @ 表示。也就是说,当读入数据的时候,碰到 @ 就要处理入口点。
移动规则:
你站在其中一块黑色的瓷砖上,只能向相邻的黑色瓷砖移动。什么意思?题目告诉我们有两种瓷砖白色(用 # 表示)与黑色(用 . 表示)。我们只能在黑色瓷砖上移动。
每次可以移动的规则呢?就是上下左右,四个方向。那么我们建立坐标系,也就是向上走,对应的 (x, y) 坐标如何变化呢?自然是水平坐标不变,垂直坐标减 1。那么用增量的方式,可以表示为 (0, -1)。那么我们可以写出四个方向的变化 (-1,0),(0,1),(1,0),(0,-1)。第一步走哪个方向没关系。
另外还有一些隐藏的规则,如不能出界。如何表示呢?就是 x 和 y 的坐标必须大于等于 0。具体看你对迷宫如何定义。
数据定义
定义 MAXN:
我们需要使用数组描述迷宫,所以老套路,定义 MAXN 表示数组的最大范围。本题定义如下:
const int MAXN = 22;
定义 BFS 辅助数据
我喜欢用结构体来定义,这样增强了代码的可读性。
根据题目要求,我们知道需要定义一个二维数组来描述迷宫本身,字符类型(char)。一个二维数组来描述每个点是否已经访问,布尔类型(bool)。一个变量描述迷宫的宽度(w)。一个变量描述迷宫的高度(h)。入口坐标(x1, y1)。出口坐标(x2, y2)。结构体的具体实现根据不同题目进行微调,整个结构体定义如下:
struct MAZE {
int w;//迷宫的宽度
int h;//迷宫的高度
bool visit[MAXN][MAXN];//描述迷宫
char data[MAXN][MAXN];//可见性描述
int x1, y1;//起点坐标
int x2, y2;//终点坐标
};
定义位置
使用结构体来描述当前位置。如下:
struct POS {
int x, y;
};
定义走法
我们使用 (x, y) 坐标来表示当前坐标,那么我们可以使用增量的形式表示每次移动方法。定义如下:
const POS move[] = {{-1,0}, {0,1}, {1,0}, {0,-1}};
注意:那个方向先走不会影响最终结果。因为我们会搜索所有可能。
下一步队列
使用 STL 的 queue 来描述。定义方法如下:
std::queue<POS> q;
建立迷宫坐标
根据输入的数据,我们可以这样建立直角坐标系,将迷宫位置进行映射。
如上图所示,我们知道起点的坐标为 (1, 7)。
下一步移动
通过一个循环,每次将当前坐标和增量坐标进行加法,就可以得到下一个坐标。比如当前坐标为 (1, 7)。
我们向左移动,增量坐标为 (-1, 0),因此下一个点坐标为 (0, 7);我们向下移动,增量坐标为 (0, 1),因此下一个点坐标为 (1, 8);我们向右移动,增量坐标为 (1, 0),因此下一个点坐标为 (2, 7);我们向上移动,增量坐标为 (0, -1),因此下一个点坐标为 (1, 6)。
代码如下:
next.x = cur.x + move[i].x;
next.y = cur.y + move[i].y;
下一个位置是否可以走
根据题目要求,我们只需要判断对应的 POS 的数据即可。如果为 # 字符,表示不可以走;如果为 . 字符,表示可以走。代码如下:
maze.data[next.x][next.y]!='#'
下一个位置是否走迷宫
很简单,我们只需要判断对应的 x 或者 y 坐标是否在 (0,0) 到 (w, h) 之内就可以。代码如下:
next.x>=0&&next.x<maze.h&&next.y>=0&&next.y<maze.w
下一个位置是否已经走过
很简单,只需要判断对应 (x, y) 的 vis 标记的值。如果为 true,说明已经走到过;如果为 false,说明没有走过。代码如下:
maze.visit[next.x][next.y]==false
算法思路
1、读入数据,并写入到合数的数据结构中。
2、找到起点位置,将起点加入到队列 q 中。
3、记录终点位置信息。
4、开始 BFS 遍历。直到找到终点或者遍历所有节点而无法到达终点。
AC 参考代码
#include <cstdio>
#include <queue>
const int MAXN = 22;
//迷宫描述
struct MAZE {
int w;//迷宫的宽度
int h;//迷宫的高度
bool visit[MAXN][MAXN];//描述迷宫
char data[MAXN][MAXN];//可见性描述
int x1, y1;//起点坐标
int x2, y2;//终点坐标
};
//位置信息描述
struct POS {
int x, y;
};
//BFS函数
int bfs(MAZE &maze) {
//四种走法描述
const POS move[] = {{-1,0}, {0,1}, {1,0}, {0,-1}};
std::queue<POS> q;//下一个访问队列
POS cur;//当前位置
POS next;//下一个位置
//插入起点
cur.x = maze.x1;
cur.y = maze.y1;
maze.visit[cur.x][cur.y] = true;//设置起点已经访问
q.push(cur);
int step = 0;
int i;
//遍历迷宫,只要队列 q 不为空就尝试走下一步
while (q.empty()==false) {
cur = q.front();//将队首节点设置为当前结点
q.pop();
step++;
//尝试从当前结点出发,走下一步,看能否移动
for (i=0; i<4; i++) {
//生成下一步位置信息
next.x = cur.x + move[i].x;
next.y = cur.y + move[i].y;
//先判断是不是终点,如果是终点,结束。
//判断下一步是否合法
if (next.x>=0&&next.x<maze.h&&next.y>=0&&next.y<maze.w&&maze.visit[next.x][next.y]==false&&maze.data[next.x][next.y]!='#') {
//可以走的节点,推入到队列 q 中
maze.visit[next.x][next.y] = true;
q.push(next);
}
}
}
return step;
}
int main() {
MAZE maze = {};//将 maze 初始化为空
scanf("%d %d", &maze.w, &maze.h);//读入迷宫的宽和高
//读入迷宫数据
int i, j;
for (i=0; i<maze.h; i++) {
for (j=0; j<maze.w; j++) {
scanf(" %c", &maze.data[i][j]);
//判断是否为起点
if (maze.data[i][j]=='@') {
maze.x1 = i;
maze.y1 = j;
}
}
}
//迷宫终点信息
//由于本题是遍历能走多少个瓷砖,而不是判断到达终点
maze.x2 = -1;
maze.y2 = -1;
printf("%d\n", bfs(maze));
return 0;
}
代码说明:
1、本题由于不是走到终点,所以在 BFS 遍历过程不需要判断是否到达终点。
2、BFS 停止的条件是队列 q 为空。也就是说没有可以继续走的节点。
3、BFS 不像前面的算法,有 100% 确定的模板。BFS 只有一个大概的思路,每题根据要求都会有所不同。