Using BFS and DFS to implement maze path finding

Reference links:

Algorithm Basics: Intuitive Explanation of BFS and DFS

Maze problem (maze problem) - depth first (DFS) and breadth first search (BFS) solution - Tencent Cloud Developer Community - Tencent Cloud

1. Concept analysis:

Introduction to BFS and DFS concepts:

BFS and DFS are two basic graph traversal algorithms.

  • Breadth-First Search (BFS, Breadth-First Search):
    • Algorithm logic: BFS starts from the starting node, explores all adjacent nodes, and then explores the adjacent nodes of these nodes, and so on. Therefore, it ensures that the first solution found is the shortest path.
    • Algorithmic efficiency: BFS is generally more efficient because it stops searching once it finds a solution. Depth-first search, on the other hand, may continue exploring even after a solution has been found.
    • Space complexity: The space complexity of BFS may be higher than that of DFS, especially when a large number of nodes need to be explored, because it needs to maintain a queue to store all nodes to be explored.
  • Depth-First Search (DFS, Depth-First Search):
    • Algorithm logic: DFS will continue to deepen along a path until it reaches the goal or there is no way to go, and then backtracks. It does not guarantee that the first solution found is the shortest path.
    • Algorithmic efficiency: DFS may be faster when the solution depth is very deep or the number of solutions is very large. But this does not apply to finding the shortest path.
    • Space complexity: DFS usually has lower space complexity because it only needs to maintain a stack to store the nodes on the current path.

Example analysis:

The picture above is a map. The gray squares are walls, the green squares are the starting point, and the red squares are the end points.

If the breadth-first search algorithm (BFS) is used here, the search logic is to start from a starting node, then visit all its adjacent nodes, then visit the adjacent nodes of these adjacent nodes, and so on, until the graph All nodes are visited. This spread is similar to how ripples spread across water. This algorithm can be understood as a radar scan, starting from a central point and scanning the surrounding area by emitting radio waves and receiving the reflected waves back.

And if you use the depth first search algorithm (DFS). The logic of the search is that DFS starts from a starting point and traverses all the points in one direction before changing the direction. This algorithm can be understood as a road to black. It starts from the root node, goes deep along a certain branch, until the end of the branch, then backtracks to the previous branch point, and then selects another branch to go deeper. This process is very similar to how people traverse tree structures in real life.

BFS diagram

DFS diagram

Code implementation of BFS and DFS:

Commonly used data structures of BFS and DFS:

A common implementation of BFS is through queues, because queues can well support the need to traverse graphs or trees hierarchically. In BFS, nodes are visited according to their distance from the starting node, so all nodes with a distance of 1 will be visited before nodes with a distance of 2, and so on. The following is a brief analysis of the application of three data structures in BFS:

queue :

  • advantage :
    • The code is simple and intuitive, and is very suitable for implementing BFS.
    • The first-in-first-out (FIFO) nature of the queue ensures that nodes are accessed in order according to their distance from the starting node.
  • shortcoming :
    • Additional space is required to store the queue.

The most common data structures used to implement depth-first search are stack and recursion. Here is a brief analysis of these two methods:

  1. recursion :

    • advantage :
      • The code is simple, clear, and intuitive. Recursion can naturally represent the search process of DFS, and each level of recursion represents the depth of the search.
      • There is no need to explicitly maintain the stack.
    • shortcoming :
      • For large graphs or trees, recursion can cause stack overflow.
      • Recursion may incur large time and space overhead.
  2. Stack :

    • advantage :
      • Using a non-recursive implementation of the stack can avoid stack overflow problems caused by recursion, especially in large-scale graphs or trees.
      • Provides greater control over the search process and memory usage.
    • shortcoming :
      • The stack and current search state need to be maintained explicitly, and the code may not be as concise as the recursive implementation.

Suppose we have the following five-by-five maze map:

S * * * *
* # * # *
* # # * *
* # # * #
* * * * E

SAs a starting point, Eas an end point, #as a wall, *as a path that can be taken. Looking at this maze, there are two paths from the start to the end:

If W、A、S、Dused to represent up, down, left, and right, the first path is followed by SSSSDDDD, and the second path is followed by DDDDSSASSD. Obviously, the path on the left is the shortest path on this map. Next, we try to use the queue of BFS and the recursion and stack of DFS to find the path from the starting point to the end point of the maze.

BFS queue algorithm implementation:

There are several points to note when writing BFS queue code:

  • Be sure to initialize an array to keep track of which nodes have been visited to avoid repeated visits and infinite loops. This note is not only for queue algorithms, but also for any map pathfinding that involves such an array.

    std::queue<Node> q;
    bool visited[rows][cols] = {false};
    
  • Use the queue's ** pushandpop ** operations to add and remove nodes. This ensures that nodes are processed in first-in-first-out (FIFO) order

    q.push(startNode);
    // ...
    Node currentNode = q.front();
    q.pop();
    
  • Handling of adjacent vertices: Before adding a vertex to the queue, check whether the vertex has already been visited to avoid repeated visits and additions.

The complete code is as follows:

#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;
}

When the code is initialized, there is only the starting point (0,0) in the queue, which is marked as visited first.

  1. Then start the loop:
    • Take out the first node (0,0) in the queue, check its unvisited neighbors, and find (0,1) and (1,0).
    • Add (0,1) and (1,0) to the queue and mark them as visited.
  2. On the next iteration of the loop:
    • Take out the first node (0,1) in the queue, check its unvisited neighbors, and find (0,2).
    • Add (0,2) to the queue and mark it as visited.
    • Next, take the second node (1,0) from the queue, look at its unvisited neighbors, find the unvisited neighbor node (2,0), add it to the queue and mark it as visited. In subsequent loops, new unvisited neighbors will continue to be checked until the target node is found or the queue is empty.

Tips: At the beginning of each cycle, the first node processed in the previous cycle will be popped out of the queue q.pop(), and it will delete qthe first element of the queue. The loop then processes the next node in the queue.

[S] * * * *        Queue: (0,0)
 * # * # *
 * # # * *
 * # # * #
 * * * * E
[S] [A] * *        Queue: (1,0)
 A # * # *
 * # # * *
 * # # * #
 * * * * E
[S] [*] * *        Queue: (0,1)
 * # * # *
 * # # * *
 * # # * #
 * * * * E
[S] [A] * *        Queue: (2,0)
 A # * # *
 [*] # * *
 * # # * #
 * * * * E

Recursive algorithm implementation of DFS:

The main thing to note about recursive algorithms is the termination condition of the recursion.

#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;
}

The logic of this code is clearer. It mainly uses the returned Boolean value to determine whether the path is correct.

DFS stack algorithm implementation:

#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();
}

Guess you like

Origin blog.csdn.net/weixin_46175201/article/details/133929515