leetcode-master/problems/kamacoder/图论广搜理论基础.md

105 lines
5.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 广度优先搜索理论基础
在[深度优先搜索](./图论深搜理论基础.md)的讲解中,我们就讲过深度优先搜索和广度优先搜索的区别。
广搜bfs是一圈一圈的搜索过程和深搜dfs是一条路跑到黑然后再回溯。
## 广搜的使用场景
广搜的搜索方式就适合于解决两个点之间的最短路径问题。
因为广搜是从起点出发,以起始点为中心一圈一圈进行搜索,一旦遇到终点,记录之前走过的节点就是一条最短路。
当然,也有一些问题是广搜 和 深搜都可以解决的,例如岛屿问题,**这类问题的特征就是不涉及具体的遍历方式,只要能把相邻且相同属性的节点标记上就行**。 (我们会在具体题目讲解中详细来说)
## 广搜的过程
上面我们提过BFS是一圈一圈的搜索过程但具体是怎么一圈一圈来搜呢。
我们用一个方格地图,假如每次搜索的方向为 上下左右不包含斜上方那么给出一个start起始位置那么BFS就是从四个方向走出第一步。
![图一](https://code-thinking-1253855093.file.myqcloud.com/pics/20220825104505.png)
如果加上一个end终止位置那么使用BFS的搜索过程如图所示
![图二](https://code-thinking-1253855093.file.myqcloud.com/pics/20220825102653.png)
我们从图中可以看出从start起点开始是一圈一圈向外搜索方格编号1为第一步遍历的节点方格编号2为第二步遍历的节点第四步的时候我们找到终止点end。
正是因为BFS一圈一圈的遍历方式所以一旦遇到终止点那么一定是一条最短路径。
而且地图还可以有障碍,如图所示:
![图三](https://code-thinking-1253855093.file.myqcloud.com/pics/20220825103900.png)
在第五步,第六步 我只把关键的节点染色了,其他方向周边没有去染色,大家只要关注关键地方染色的逻辑就可以。
从图中可以看出如果添加了障碍我们是第六步才能走到end终点。
只要BFS只要搜到终点一定是一条最短路径大家可以参考上面的图自己再去模拟一下。
## 代码框架
大家应该好奇,这一圈一圈的搜索过程是怎么做到的,是放在什么容器里,才能这样去遍历。
很多网上的资料都是直接说用队列来实现。
其实,我们仅仅需要一个容器,能保存我们要遍历过的元素就可以,**那么用队列,还是用栈,甚至用数组,都是可以的**。
**用队列的话,就是保证每一圈都是一个方向去转,例如统一顺时针或者逆时针**
因为队列是先进先出,加入元素和弹出元素的顺序是没有改变的。
**如果用栈的话,就是第一圈顺时针遍历,第二圈逆时针遍历,第三圈有顺时针遍历**
因为栈是先进后出,加入元素和弹出元素的顺序改变了。
那么广搜需要注意 转圈搜索的顺序吗? 不需要!
所以用队列,还是用栈都是可以的,但大家都习惯用队列了,**所以下面的讲解用我也用队列来讲,只不过要给大家说清楚,并不是非要用队列,用栈也可以**。
下面给出广搜代码模板,该模板针对的就是,上面的四方格的地图: (详细注释)
```CPP
int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 表示四个方向
// grid 是地图,也就是一个二维数组
// visited标记访问过的节点不要重复访问
// x,y 表示开始搜索节点的下标
void bfs(vector<vector<char>>& grid, vector<vector<bool>>& visited, int x, int y) {
queue<pair<int, int>> que; // 定义队列
que.push({x, y}); // 起始节点加入队列
visited[x][y] = true; // 只要加入队列,立刻标记为访问过的节点
while(!que.empty()) { // 开始遍历队列里的元素
pair<int ,int> cur = que.front(); que.pop(); // 从队列取元素
int curx = cur.first;
int cury = cur.second; // 当前节点坐标
for (int i = 0; i < 4; i++) { // 开始想当前节点的四个方向左右上下去遍历
int nextx = curx + dir[i][0];
int nexty = cury + dir[i][1]; // 获取周边四个方向的坐标
if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 坐标越界了,直接跳过
if (!visited[nextx][nexty]) { // 如果节点没被访问过
que.push({nextx, nexty}); // 队列添加该节点为下一轮要遍历的节点
visited[nextx][nexty] = true; // 只要加入队列立刻标记,避免重复访问
}
}
}
}
```
## 总结
当然广搜还有很多细节需要注意的地方,后面我会针对广搜的题目还做针对性的讲解。
**因为在理论篇讲太多细节,可能会让刚学广搜的录友们越看越懵**,所以细节方面针对具体题目在做讲解。
本篇我们重点讲解了广搜的使用场景,广搜的过程以及广搜的代码框架。
其实在二叉树章节的[层序遍历](https://programmercarl.com/0102.%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%B1%82%E5%BA%8F%E9%81%8D%E5%8E%86.html)中,我们也讲过一次广搜,相当于是广搜在二叉树这种数据结构上的应用。
这次则从图论的角度上再详细讲解一次广度优先遍历。
相信看完本篇,大家会对广搜有一个基础性的认识,后面再来做对应的题目就会得心应手一些。