BFS模板

18 篇文章 3 订阅

前文,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 只有一个大概的思路,每题根据要求都会有所不同。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

努力的老周

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值