广度优先搜索

34 篇文章 1 订阅
34 篇文章 2 订阅

3.1. 队列
这一节我们先学习一个新的数据结构——队列。
队列(queue) 是一种线性的数据结构,和栈一样是一种运算受限制的线性表。其限制只允许从表的
前端(front)进行删除操作,而在表的后端(rear)进行插入操作。一般允许进行插入的一端我们称为
队尾,允许删除的一端称为队首。队列的插入操作又叫入队,队列的删除操作又叫出队。
可以把队列想象成购物时排队结账的队伍,先排队的人会先结账,后排队的人会后结账,并且不允许
有插队的行为,只能在队伍的末尾进行排队。这就是队列的特点,具有 先进先出 的性质,而这一点和
栈的 先进后出 正好相反。
队列的结构如下图所示:
队列的主要操作包括:
入队(push)
出队(pop)
判断队列是否为空(empty)
统计队列元素的个数(size)
访问队首元素(front)
在 C++ 的标准模板库中,队列也有已经实现好了模板。下面我们介绍 C++ 中的队列的使用。
3.1.1. 引用库
C++ 中 queue 的实现在一个 头文件中,在代码开头引入这个头文件,并在引入所有头文件之
后加上一句 using namespace std 。

#include <queue>
using namespace std;
int main() {
return 0;
}

3.1.2. 构造一个队列
现在我们来构造一个队列。
C++ 中直接构造一个 queue 的语句为: queue vec 。这样我们定义了一个名为 vec 的储存 T 类型数
据的队列。其中 T 是我们数组要储存的数据类型,可以是 int 、 float 、 double 或者其他自定义的数
据类型等等。初始的时候 vec 是空的。 比如 queue q 定义了一个储存整数的队列 q 。
3.1.3. 入队
通过 push() 方法在队尾插入一个新的元素。

#include <queue>
using namespace std;
int main() {
queue<int> q;
q.push(1);
q.push(2);
q.push(3);
return 0;
}

3.1.4. 获取队首元素
通过 front() 方法可以获取到当前的队首元素。

#include <queue>
#include <iostream>
using namespace std;
int main() {
queue<int> q;
q.push(1);
cout << q.front() << endl;
q.push(2);
cout << q.front() << endl;
q.push(3);
cout << q.front() << endl;
return 0;
}

3.1.5. 出队
通过 pop() 方法可以让队首元素出队。

#include <queue>
#include <iostream>
using namespace std;
int main() {
queue<int> q;
vec.push(1);
vec.push(2);
vec.push(3);
q.pop();
cout << q.front() << endl;
q.pop();
cout << q.front() << endl;
q.pop();
return 0;
}

3.1.6. 判断队列是否为空
empty() 方法可以判断队列是否为空,如果为空,方法返回 true ,否则返回 false 。这个方法的意义
是,在我们每次调用 front() 和 pop() 之前,都要检查一下,保证队列不为空,否则去访问一个空队列
的首部或者让一个空的队列出队就会发生错误。

#include <queue>
#include <iostream>
using namespace std;
int main() {
queue<int> q;
vec.push(1);
vec.push(2);
vec.push(3);
while (!q.empty()) {
// 如果队列不空,一直出队,
// 用这样的方法清空一个队列,原因是queue没有 clear 方法。
cout << q.front() << endl;
q.pop();
}
return 0;
}

3.1.7. 清空
队列比较特殊,并没有 clear() 方法,而清空一个队列,需要手动清空。
// 如果队列不空,用这样的方法清空一个队列

while (!q.empty()) {
	q.pop();
}

【实践操作】队列的使用
这一节我们简单的学习一下 C++ 标准模板库中的 queue 的使用,第一步我们在头部记上头文
件 。
在头部引入头文件
#include
接下来创建一个空的队列。
在 main 函数里面写下

queue<string> q;

依次把 “zhangsan” 、 “lisi” 、 “wangwu” 入队。

q.push("zhangsan");
q.push("lisi");
q.push("wangwu");

然后把队列中的所有元素都输出一遍,并且出队。

while (!q.empty()) {
cout << q.front() << endl;
q.pop();
}

这一节已经完成,点击 运行 查看结果。
【小练习】报数游戏
有 个小朋友做游戏,他们的编号分别是 。他们按照编号从小到大依次顺时针围成一个圆
圈,第一个小朋友从 开始报数,依次按照顺时针方向报数(报数的值加一),每个报 的人会离开
队伍,然后下一个小朋友会继续从 开始报数,直到只剩一个小朋友为止。
最后剩下的小朋友的编号为 ________________。
【实践操作】实现报数游戏
这一节我们借助队列来辅助完成报数游戏的计算。为了使得我们写的程序更通用,我们假设有 个小
朋友,报到 的小朋友退出游戏,首先定义两个变量,然后从标准输入输入它们。
在 main 函数里面写下
7 1, 2, 3…7
1 5
1
n
m

int n, m;
cin >> n >> m;
这一步我们定义一个 int 类型的队列 q 。
在 main 函数里面接着刚才的输入写下

queue<int> q;

记得在开头 加上头文件 哦
我们知道他们的报数顺序为 到 ,我们按照这个顺序把它们依次压入队列。
在 queue q; 下面写下

for (int i = 1; i <= n; i++) {
	q.push(i);
}

现在我们开始模拟报数过程,定义一个变量 cur 来表示当前正在报的数,初始的时候从 开始。当队
列中剩下的人数大于 的时候,游戏都还在进行。
从队首取一个小朋友,如果这个小朋友报数正好是 ,那他就退出游戏,然后下一个小朋友从 开始
报数。
在 main 函数里面继续写下

int cur = 1;
while (q.size() > 1) {
	int x = q.front();
	q.pop();
	if (cur == m) {
		cur = 1;
	}
}

那如果这个小朋友报的数不是 ,那么他还需要继续游戏,他刚报完数,需要等过一圈以后才会再次
报数,我们让这个小朋友重新入队。
在 while 循环里面写下另外一种情况

else {
	q.push(x);
	cur++;
}

最后我们输出剩下的小朋友的编号。
1 n
1
1
m 1
m
在已经完成的 while 循环下面写下
cout << q.front() << endl;
小程序已经完成,点击 运行,输入 7 5 可以得出上一个填空题的结果。
3.2. 广度优先搜索
广度优先搜索,又称宽度优先搜索,简称 bfs,我们以后都会用 bfs 来表示广度优先搜索。与深度优先
搜索不同的是,广度优先搜索会先将与起始点距离较近的点搜索完毕,再继续搜索较远的点,而深搜
却是沿着一个分支搜到最后。
bfs 从起点开始,优先搜索离起点最近的点,然后由这个最近的点扩展其他稍近的点,这样一层一层的
扩展,就像水波扩散一样。
A
B C D
E F G
对上图进行深搜按照顶点访问顺序会得到序列:
对上图进行广搜按照顶点访问顺序会得到序列:
广度优先搜索的层次关系是很明显的,上面的图的分层次关系如下:
第一层的点为
第二层的点为
第三层的点为
bfs 需要借助队列来实现:

  1. 初始的时候把起始点放到队列中,并标记起点访问。

  2. 如果队列不为空,从队列中取出一个元素 ,否则算法结束。

  3. 访问和 相连的所有点 ,如果 没有被访问,把 入队,并标记已经访问。

  4. 重复执行步骤 2。
    最后写出来的代码框架如下:
    A − B − E − F − C − D − G
    A − B − C − D − E − F − G
    A
    B, C, D
    E, F, G
    x
    x v v v

    void bfs(起始点) {
    将起始点放入队列中;
    标记起点访问;
    while (如果队列不为空) {
    访问队列中队首元素x;
    删除队首元素;
    for (x 所有相邻点) {
    if (该点未被访问过且合法) {
    将该点加入队列末尾;
    }
    }
    }
    队列为空,广搜结束;
    }
    【小练习】bfs 搜索顺序
    对于下面这张图,可能的 bfs 顺序有哪些?
    A
    B C D
    E F G
    A­B­C­D­E­F­G
    A­B­E­F­C­D­G
    A­B­D­C­E­F­G
    3.3. 迷宫游戏
    我们先来玩一个迷宫游戏,尝试走一下面的迷宫。

这是一种最短的走法:
我们用一个二维的字符数组来表示前面画出的迷宫:
S**.

***T
其中字符 S 表示起点,字符 T 表示终点,字符 * 表示墙壁,字符 . 表示平地。你需要从 S 出发走
到 T ,每次只能向上下左右相邻的位置移动,不能走出地图,也不能穿过墙壁,每个点只能通过一
次。给你一个具体的迷宫,你需要求出从起点到终点最短的路径需要走多少步。

我们可以借助 bfs 来求解迷宫游戏。由于 bfs 是分层搜索,因此,第一次搜索到终点的时候,当前搜索
的层数就是最短路径的长度。
【实践操作】bfs 求解迷宫游戏

#include <iostream>
#include <string>
#include <queue>
using namespace std;
int n, m;
string maze[110];
bool vis[110][110];
int dir[4][2] = {{‐1, 0}, {0, ‐1}, {1, 0}, {0, 1}};
bool in(int x, int y) {
	return 0 <= x && x < n && 0 <= y && y < m;
}
int main() {
	cin >> n >> m;
	for (int i = 0; i < n; i++) {
		cin >> maze[i];
	}
	int x, y;
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < m; j++) {
			if (maze[i][j] == 'S') {
				x = i, y = j;
			}
		}
	}
	return 0;
}

为了记录一个状态,首先我们定义一个结构体,用这个结构体来记录一个状态。这个结构体需要记录
坐标 x, y 以及当前使用的步数 d 。
我们为这个结构体加上一个构造函数,后面你将会见识到构造函数的方便之处。
在 main 函数的上面写下

struct node {
int x, y, d;
	node(int xx, int yy, int dd) {
		x = xx;
		y = yy;
		d = dd;
	}
};

2018/7/5 3.广度优先搜索
file:///C:/Users/11027/AppData/Local/Temp/mume11865­8740­1b9tgfl.omui.html 10/13
我们开始实现 bfs 搜索,bfs 函数传入两个参数 sx, sy 表示起点的坐标,在 bfs 开头先定义一个队列,
然后把起点压入队列,并且标记起点为访问状态,这里我们借助构造函数 node(sx, sy, 0) 快速构建一
个结构体。
在 main 函数上方写下

int bfs(int sx, int sy) {
queue<node> q;
q.push(node(sx, sy, 0));
vis[sx][sy] = true;
}

当队列中有元素的时候,我们先取出队首元素。
在 bfs 函数里面继续写下

while (!q.empty()) {
node now = q.front();
q.pop();
}

用我们从队列中取出点去扩展其他的点。还记得这种二维地图上通过枚举方向变量来枚举一个方向的
方法吧。
在 bfs 函数的 while 循环里面继续写下

for (int i = 0; i < 4; i++) {
int tx = now.x + dir[i][0];
int ty = now.y + dir[i][1];
}

如果 合法并且没有被访问,如果是终点,那么可以直接返回了,如果不是终点,把这个点标
记为访问,并且压入队列。
在 bfs 函数的 for 循环里面继续写下

if (in(tx, ty) && maze[tx][ty] != '*' && !vis[tx][ty]) {
if (maze[tx][ty] == 'T') {
return now.d + 1;
} else {
vis[tx][ty] = true;
q.push(node(tx, ty, now.d + 1));
}
}
(tx,ty)

如果没有通过 BFS 访问到终点,需要返回 ,表示没有找到一条从起点到终点的路径,自然也就不
存在最短路了。
return ‐1;
最后,我们只需要在 main 函数中调用 bfs 函数即可。
在 main 函数 return 0 上面写下:
cout << bfs(x, y) << endl;
这一节已经完成,点击 运行,输入下面的数据:
5 6
…S*
.
..
*…
.
.T…
【例题1】一维坐标的移动
在一个长度为 的坐标轴上,蒜头君想从 点 移动到 点。他的移动规则如下:

  1. 向前一步,坐标增加 。
  2. 向后一步,坐标减少 。
  3. 跳跃一步,使得坐标乘 。
    蒜头君不能移动到坐标小于 或大于 的位置。蒜头君想知道从 点移动到 点的最少步数是多
    少,你能帮他计算出来么?
    输入格式
    第一行输入三个整数 , , ,分别代表坐标轴长度,起始点坐标,终点坐标。(

    输出格式
    输出一个整数占一行,代表蒜头要走的最少步数。
    样例输入
    10 2 7
    样例输出
    3
    −1
    n A B
    1
    1
    2
    0 n A B
    n A B
    0 ≤ A, B ≤ n ≤ 5000

【实践操作】bfs求图的连通块

#include <iostream>
#include <vector>
#include <queue>
using namespace std;
vector<int> G[10010];
int main() {
return 0;
}

在这一节,我们来学习用 bfs 来求解图的连通块的数量。为了强化邻接表的使用,我们这里用邻接表
来储存输入的图。
在 main 函数开始写下:

int n, m;
cin >> n >> m;
for (int i = 0; i < m; i++) {
int a, b;
cin >> a >> b;
G[a].push_back(b);
G[b].push_back(a);
}

我们用一个数组 vis 来标记每个点是否已经访问,对于没有访问过的点,我们通过 bfs 搜索出来这个
点对应的连通块。
在 main 函数里面继续写下:

int cnt = 0;
for (int i = 1; i <= n; i++) {
	if (!vis[i]) {
		cnt++;
		bfs(i);
	}
}

然后,我们在 main 函数上方、 vector G[10010]; 的下方添加一行:
bool vis[10010];
这一步我们开始实现 bfs 函数。在 bfs 函数开头定义一个队列,然后把起点压入队列,并且标记为访
问。
在 main 函数上方写下
2018/7/5 3.广度优先搜索

void bfs(int st) {
	queue<int> q;
	q.push(st);
	vis[st] = true;
}

当队列不为空的时候,取出队首的点,然后通过连接的边去扩展出其他的点。
在 bfs 函数里面继续写下

while (!q.empty()) {
	int x = q.front();
	q.pop();
	for (int i = 0; i < G[x].size(); i++) {
		int v = G[x][i];
		if (!vis[v]) {
			vis[v] = true;
			q.push(v);
		}
	}
}

最后我们在 main 函数的 return 0; 之前输出连通块的个数。
cout << cnt << endl;
这一节已经完成了,点击 运行,输入下面的数据,看一看程序运行的结果吧。
6 3
1 2
2 3
4 5

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值