参考链接:
迷宫问题(maze problem)——深度优先(DFS)与广度优先搜索(BFS)求解-腾讯云开发者社区-腾讯云
一、概念解析:
BFS和DFS概念简介:
BFS和DFS是两种基本的图遍历算法。
- 广度优先搜索(BFS, Breadth-First Search):
- 算法逻辑:BFS从起始节点开始,探索所有的相邻节点,然后再探索这些节点的相邻节点,依此类推。因此,它确保找到的第一个解是最短的路径。
- 算法效率:BFS通常更高效,因为它一旦找到解就会停止搜索。而深度优先搜索可能会继续探索,即使已经找到解。
- 空间复杂度:BFS的空间复杂度可能会比DFS高,特别是在需要探索大量节点的情况下,因为它需要维护一个队列来存储所有待探索的节点。
- 深度优先搜索(DFS, Depth-First Search):
- 算法逻辑:DFS则会沿着一条路径不断深入,直到到达目标或者无路可走为止,然后再回溯。它不保证找到的第一个解是最短路径。
- 算法效率:在解的深度非常深或者解的数量非常多的情况下,DFS可能会更快。但这并不适用于寻找最短路径。
- 空间复杂度:DFS通常具有较低的空间复杂度,因为它只需要维护一个栈来存储当前路径上的节点。
举例分析:
上图是一张地图,灰色的方块为墙壁,绿色方块为起点,红色方块为终点。
这里如果使用广度优先搜索算法(BFS),搜索的逻辑是从一个起始节点开始,然后访问所有与其相邻的节点,接着访问这些相邻节点的相邻节点,以此类推,直到图中的所有节点都被访问。这种扩展方式类似于波纹在水面上的扩散。这种算法可以理解为雷达扫描,从一个中心点开始,通过发出无线电波并接收反射回来的波来扫描周围区域。
而如果使用深度优先搜索算法(DFS)。搜索的逻辑是,DFS从一个起点出发,先把一个方向的点都遍历完才会改变方向这种算法可以理解为一条路走到黑。它从根节点开始,沿着某一分支深入,直至该分支的末端,然后回溯到前一个分叉点,再选另一分支深入。这个过程与人们在现实生活中遍历树形结构的方式非常相似。
BFS示意图
DFS示意图
BFS和DFS的代码实现:
BFS和DFS常用的数据结构:
BFS的常见实现是通过队列来完成的,因为队列能够很好地支持按层次遍历图或树的需求。在BFS中,节点会按照它们离起始节点的距离被访问,所以所有距离为1的节点会先于距离为2的节点被访问,以此类推。下面是对三种数据结构在BFS中应用的简要分析:
队列:
- 优点:
- 代码简单、直观,非常适合于实现BFS。
- 队列的先进先出(FIFO)特性能确保节点按照它们离起始节点的距离顺序被访问。
- 缺点:
- 需要额外的空间来存储队列。
实现深度优先搜索最常见的数据结构是栈和递归。下面是对这两种方法的简要分析:
-
递归:
- 优点:
- 代码简单、清晰、直观。递归能自然地表示DFS的搜索过程,每一层递归代表搜索的深度。
- 不需要显式地维护栈。
- 缺点:
- 对于大规模的图或树,递归可能导致栈溢出。
- 递归可能会产生较大的时间和空间开销。
- 优点:
-
栈:
- 优点:
- 使用栈的非递归实现可以避免递归带来的栈溢出问题,特别是在大规模的图或树中。
- 可以更好地控制搜索过程和内存使用。
- 缺点:
- 需要显式地维护栈和当前搜索状态,代码可能不如递归实现简洁。
- 优点:
假设有如下一张五乘五的迷宫地图:
S | * | * | * | * |
---|---|---|---|---|
* | # | * | # | * |
* | # | # | * | * |
* | # | # | * | # |
* | * | * | * | E |
S
为出发点,E
为终点,#
为墙壁,*
为可以走的路径。看这个迷宫,起点到终点有两条路径分别是:
如果用W、A、S、D
来表示上下左右,第一条路径的走法就是SSSSDDDD
、而第二种路径的走法就是DDDDSSASSD
。很明显左图的走法为这个地图的最短路径,接下来我们尝试使用BFS的队列以及DFS的递归与栈分别实现寻找迷宫从起点到终点的路径。
BFS的队列算法实现:
编写BFS的队列代码时有几点需要注意:
-
一定要初始化一个数组来跟踪哪些节点已经被访问过,以避免重复访问和无限循环。这个注意事项不仅仅是队列算法,凡是涉及到地图寻路都要有这么一个数组。
std::queue<Node> q; bool visited[rows][cols] = {false};
-
使用队列的**
push
和pop
**操作来添加和移除节点。这保证了节点以先进先出(FIFO)的顺序被处理q.push(startNode); // ... Node currentNode = q.front(); q.pop();
-
邻接顶点的处理:在将顶点加入队列之前,检查该顶点是否已被访问,以避免重复访问和加入。
完整代码如下:
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
char maze[5][5]={
{'S','*','*','*','*'},
{'*','#','*','#','*'},
{'*','#','#','*','*'},
{'*','#','#','*','#'},
{'*','*','*','*','E'}
};
struct Point {
int row, col;
string path; // 路径跟踪变量
};
bool isValid(int row, int col) {
// 检查点是否在地图内并且是可行走的
return row >= 0 && row < 5 && col >= 0 && col < 5 && maze[row][col] != '#';
}
void bfs() {
queue<Point> q;
q.push({0, 0, ""}); // 将起点放入队列中
bool visited[5][5] = {false};
visited[0][0] = true; // 该变量用于记录图中的每个节点是否已被访问过
while (!q.empty()) {
Point p = q.front();
q.pop();
int row = p.row;
int col = p.col;
string path = p.path;
// 判断是否到终点了
if (maze[row][col] == 'E') {
cout << path << endl;
return;
}
// 检查每个方向,如果有效就排队
if (isValid(row - 1, col) && !visited[row - 1][col]) {
q.push({row - 1, col, path + 'W'});
visited[row - 1][col] = true;
}
if (isValid(row + 1, col) && !visited[row + 1][col]) {
q.push({row + 1, col, path + 'S'});
visited[row + 1][col] = true;
}
if (isValid(row, col - 1) && !visited[row][col - 1]) {
q.push({row, col - 1, path + 'A'});
visited[row][col - 1] = true;
}
if (isValid(row, col + 1) && !visited[row][col + 1]) {
q.push({row, col + 1, path + 'D'});
visited[row][col + 1] = true;
}
}
cout << "No path found" << endl;
}
int main() {
bfs();
return 0;
}
代码在初始化时,队列中只有起点 (0,0),先将其标记为已访问。
- 然后开始循环:
- 取出队列中的第一个节点 (0,0),查看其未访问的邻居,发现 (0,1) 和 (1,0)。
- 将 (0,1) 和 (1,0) 加入队列,并标记为已访问。
- 在下一轮循环中:
- 取出队列中的第一个节点 (0,1),查看其未访问的邻居,发现 (0,2)。
- 将 (0,2) 加入队列,并标记为已访问。
- 接下来,取出队列中的第二个节点 (1,0),查看其未访问的邻居,发现未访问的邻居节点 (2,0),将其加入队列中并将其标记为已访问。在之后的循环中,会继续检查新的未访问邻居,直到找到目标节点或者队列为空。
tips:每一轮循环开始时,都会从队列中弹出上一轮循环中处理的第一个节点 q.pop()
,它会删除队列 q
的第一个元素。然后,该轮循环会处理队列中的下一个节点。
[S] * * * * Queue: (0,0)
* # * # *
* # # * *
* # # * #
* * * * E
[S] [A] * * Queue: (1,0)
A # * # *
* # # * *
* # # * #
* * * * E
[S] [*] * * Queue: (0,1)
* # * # *
* # # * *
* # # * #
* * * * E
[S] [A] * * Queue: (2,0)
A # * # *
[*] # * *
* # # * #
* * * * E
DFS的递归算法实现:
关于递归算法需要注意的主要就是递归的终止条件。
#include <iostream>
#include <vector>
using namespace std;
char maze[5][5] = {
{'S','*','*','*','*'},
{'*','#','*','#','*'},
{'*','#','#','*','*'},
{'*','#','#','*','#'},
{'*','*','*','*','E'}
};
bool visited[5][5] = {false};
vector<char> path;
bool dfs(int row, int col) {
// 如果超出边界或遇到墙壁或已访问过该点,返回false
if (row < 0 || row >= 5 || col < 0 || col >= 5 || maze[row][col] == '#' || visited[row][col]) {
return false;
}
// 标记当前位置为已访问
visited[row][col] = true;
// 如果找到终点,返回true
if (maze[row][col] == 'E') {
return true;
}
// 递归探索四个方向
if (dfs(row - 1, col)) { // 上
path.push_back('W');
return true;
}
if (dfs(row + 1, col)) { // 下
path.push_back('S');
return true;
}
if (dfs(row, col - 1)) { // 左
path.push_back('A');
return true;
}
if (dfs(row, col + 1)) { // 右
path.push_back('D');
return true;
}
// 如果所有方向都无法找到路径,回溯
visited[row][col] = false;
return false;
}
int main() {
dfs(0, 0); // 从起点开始递归
// 输出路径
for (int i = path.size() - 1; i >= 0; i--) { // 从尾到头输出,因为我们是在递归返回时添加的步骤
cout << path[i];
}
cout << endl;
return 0;
}
这个代码逻辑就清晰一些,主要是通过返回的布尔值来判断路径是否正确。
DFS的栈算法实现:
#include <iostream>
#include <stack>
using namespace std;
struct Point{
//行与列
int row;
int col;
Point(int x,int y){
this->row=x;
this->col=y;
}
bool operator!=(const Point& rhs){
if(this->row!=rhs.row||this->col!=rhs.col)
return true;
return false;
}
};
//func:获取相邻未被访问的节点
//para:mark:结点标记,point:结点,m:行,n:列
//ret:邻接未被访问的结点
Point getAdjacentNotVisitedNode(bool** mark,Point point,int m,int n){
Point resP(-1,-1);
if(point.row-1>=0&&mark[point.row-1][point.col]==false){//上节点满足条件
resP.row=point.row-1;
resP.col=point.col;
return resP;
}
if(point.col+1<n&&mark[point.row][point.col+1]==false){//右节点满足条件
resP.row=point.row;
resP.col=point.col+1;
return resP;
}
if(point.row+1<m&&mark[point.row+1][point.col]==false){//下节点满足条件
resP.row=point.row+1;
resP.col=point.col;
return resP;
}
if(point.col-1>=0&&mark[point.row][point.col-1]==false){//左节点满足条件
resP.row=point.row;
resP.col=point.col-1;
return resP;
}
return resP;
}
//func:给定二维迷宫,求可行路径
//para:maze:迷宫;m:行;n:列;startP:开始结点 endP:结束结点; pointStack:栈,存放路径结点
//ret:无
void mazePath(void* maze,int m,int n,const Point& startP,Point endP,stack<Point>& pointStack){
//将给定的任意列数的二维数组还原为指针数组,以支持下标操作
int** maze2d=new int*[m];
for(int i=0;i<m;++i){
maze2d[i]=(int*)maze+i*n;
}
if(maze2d[startP.row][startP.col]==1||maze2d[endP.row][endP.col]==1)
return ; //输入错误
//建立各个节点访问标记
bool** mark=new bool*[m];
for(int i=0;i<m;++i){
mark[i]=new bool[n];
}
for(int i=0;i<m;++i){
for(int j=0;j<n;++j){
mark[i][j]=*((int*)maze+i*n+j);
}
}
//将起点入栈
pointStack.push(startP);
mark[startP.row][startP.col]=true;
//栈不空并且栈顶元素不为结束节点
while(pointStack.empty()==false&&pointStack.top()!=endP){
Point adjacentNotVisitedNode=getAdjacentNotVisitedNode(mark,pointStack.top(),m,n);
if(adjacentNotVisitedNode.row==-1){ //没有未被访问的相邻节点
pointStack.pop(); //回溯到上一个节点
continue;
}
//入栈并设置访问标志为true
mark[adjacentNotVisitedNode.row][adjacentNotVisitedNode.col]=true;
pointStack.push(adjacentNotVisitedNode);
}
}
int main(){
int maze[5][5]={
{0,0,0,0,0},
{0,1,0,1,0},
{0,1,1,0,0},
{0,1,1,0,1},
{0,0,0,0,0}
};
Point startP(0,0);
Point endP(4,4);
stack<Point> pointStack;
mazePath(maze,5,5,startP,endP,pointStack);
//没有找打可行解
if(pointStack.empty()==true)
cout<<"no right path"<<endl;
else{
stack<Point> tmpStack;
cout<<"path:";
while(pointStack.empty()==false){
tmpStack.push(pointStack.top());
pointStack.pop();
}
while (tmpStack.empty()==false){
printf("(%d,%d) ",tmpStack.top().row,tmpStack.top().col);
tmpStack.pop();
}
}
getchar();
}