在路径规划方面,迷宫搜索是验证图遍历算法的标准场景,这里将简单介绍一下四种核心算法:深度优先搜索(DFS)、广度优先搜索(BFS)、Dijkstra 算法以及 A* 算法的原理以及C++的实现。

在无权图即迷宫各路径代价相等中,DFS和BFS是两种最基本的策略。

一、迷宫的定义


1.基础常量与数据结构

迷宫的每个节点有 4 个移动方向,先定义核心常量:

#include <iostream>
#include <stack>

// 迷宫每个节点的4个方向
const int RIGHT = 0;
const int DOWN = 1;
const int LEFT = 2;
const int UP = 3;

// 迷宫每个节点方向的数量
const int WAY_NUM = 4;

// 节点的行走状态
const int YES = 4;
const int NO = 5;

    // 迷宫节点信息
struct Node
 {
        int x;
        int y;
        int val;
        int state[WAY_NUM];
};

2. 迷宫基础模型

迷宫类定义如下:

#include <iostream>
#include <stack>
 
class Maze
{
public:
    Maze(int _col, int _row) : col(_col), row(_row)
    {
        pMaze = new Node *[row];
        for (size_t i = 0; i < row; i++)
        {
            pMaze[i] = new Node[col];
        }
    }

    // 析构函数:释放内存
    ~Maze() {
        for (size_t i = 0; i < row; i++) {
            delete[] pMaze[i];
        }
        delete[] pMaze;
    }

    void initNode(int x, int y, int val)
    {
        pMaze[x][y].x = x;
        pMaze[x][y].y = y;
        pMaze[x][y].val = val;
        for (int i = 0; i < WAY_NUM; ++i)
        {
            pMaze[x][y].state[i] = NO;
        }
    }

    // 初始化迷宫0节点四个方向的行走状态信息
    void setNodeState()
    {
        for (int i = 0; i < row; ++i)
        {
            for (int j = 0; j < col; ++j)
            {
                if (pMaze[i][j].val == 1)
                {
                    continue;
                }
                // 当前节点的右边,值如果是0,表示可以走
                // 右:判断列不越界且右侧节点可走
                if (j < col - 1 && pMaze[i][j + 1].val == 0)
                {
                    pMaze[i][j].state[RIGHT] = YES;
                }

                // 下:判断行不越界且下侧节点可走
                if (i < row - 1 && pMaze[i + 1][j].val == 0)
                {
                    pMaze[i][j].state[DOWN] = YES;
                }
 
                // 左:判断列不越界且左侧节点可走
                if (j > 0 && pMaze[i][j - 1].val == 0)
                {
                    pMaze[i][j].state[LEFT] = YES;
                }

                // 上:判断行不越界且上侧节点可走
                if (i > 0 && pMaze[i - 1][j].val == 0)
                {
                    pMaze[i][j].state[UP] = YES;
                }
            }
        }
    }

private:
    int col;                              // 列数
    int row;                            // 行数
    Node **pMaze;                //  迷宫二维数组
    std::stack<Node> stack;  // 保存DFS路径的栈
};

以 6×6 迷宫为例,迷宫定义如下:

0 0 1 1 1 1
1 0 0 0 0 1
1 0 1 1 0 1
1 0 0 0 0 1
1 0 1 1 1 1
1 0 0 0 0 0

0表示通路,1表示墙壁,迷宫入口为 (0,0),迷宫出口为:(5,5)。

二、深度优先搜索


深度优先搜索(Depth First Search,DFS)优先访问节点的后继节点。在迷宫中,它沿着一条路径持续推进,直到遇到障碍或者边界,随后通过回溯,返回上一个分叉点。

它的实现方式是使用递归或栈。

#include <iostream>
#include <stack>
 
class Maze
{

public:
    Maze(int _col, int _row) : col(_col), row(_row)
    {
       //同上,略
    }
    void initNode(int x, int y, int val)
    {
       //同上,略
    }

    // 初始化迷宫0节点四个方向的行走状态信息
    void setNodeState()
    {
        //同上,略
    }

    void searchMazePath()
    {
        //  没有入口,直接return
        if (pMaze[0][0].val == 1)
        {
            return;
        }
        stack.push(pMaze[0][0]);

        while (!stack.empty())
        {
            Node top = stack.top();
            int x = top.x;
            int y = top.y;

            // 已经找到了右下角的迷宫出口
            if (x == row - 1 && y == col - 1)
            {
                return;
            }

            // 先往右找
            if (pMaze[x][y].state[RIGHT] == YES)
            {
                pMaze[x][y].state[RIGHT] = NO;
                pMaze[x][y + 1].state[LEFT] = NO;
                stack.push(pMaze[x][y + 1]); // 如果右边能走,就入栈
                continue;                    // 回到开头,重新去上面入栈的右边,继续判断
            }
            // 如果右边走不了,就往下
            if (pMaze[x][y].state[DOWN] == YES)
            {
                pMaze[x][y].state[DOWN] = NO;
                pMaze[x + 1][y].state[UP] = NO;
                stack.push(pMaze[x + 1][y]);
                continue;
            }
            // 往左查找
            if (pMaze[x][y].state[LEFT] == YES)
            {
                pMaze[x][y].state[LEFT] = NO;
                pMaze[x][y - 1].state[RIGHT] = NO;
                stack.push(pMaze[x][y - 1]);
                continue;
            }
            // 往上查找
            if (pMaze[x][y].state[UP] == YES)
            {
                pMaze[x][y].state[UP] = NO;
                pMaze[x - 1][y].state[DOWN] = NO;
                stack.push(pMaze[x - 1][y]);
                continue;
            }

            // 如果4个方向都不能走,说明栈顶元素节点没有方向可走,出栈
            stack.pop();
        }
    }

    void showMazePath()
    {
        if (stack.empty())
        {
            std::cout << "不存在一条迷宫路径" << std::endl;
        }
        else
        {
            std::cout << "-------迷宫路径-------" << std::endl;
            while (!stack.empty())
            {
                Node top = stack.top();
                pMaze[top.x][top.y].val = '*';
                stack.pop();
            }

            for (int i = 0; i < row; ++i)
            {
                for (int j = 0; j < col; j++)
                {
                    if (pMaze[i][j].val == '*')
                    {
                        std::cout << "* ";
                    }
                    else
                    {
                        std::cout << pMaze[i][j].val << " ";
                    }
                }
                std::cout << std::endl;
            }
        }
    }

private:
    // 迷宫节点信息
    struct Node
    {
        int x;
        int y;
        int val;
        int state[WAY_NUM];
    };

    int col;
    int row;
    Node **pMaze;           // 动态生成迷宫
    std::stack<Node> stack; // 辅助生成深度优先查找到的迷宫的路径
};
 

上述代码的核心逻辑:

  • 栈的作用:栈用于保存当前行走的路径,栈顶始终是当前所在的节点。
  • 方向优先级:代码中按 "右→下→左→上" 的顺序尝试,DFS 会优先沿着一个方向钻到底。
  • 回溯机制:当当前节点四个方向都走不通时,执行pop()出栈,回到上一个节点继续尝试其他方向。
  • 防回头标记:将走过的路径双向标记为不可走(如向右走后,当前节点的 RIGHT 和右侧节点的 LEFT 都设为 false),避免重复走同一路径。

调用方法如下:

int main()
{
    cout << "请输入迷宫的行列数,例如:10 10:";
    int row, col, data;
    cin >> row >> col;
    Maze maze(row, col); //
    cout << "请输入迷宫的路径信息(0表示可以走,1表示不能走)" << endl;
    for (size_t i = 0; i < row; ++i)
    {
        for (size_t j = 0; j < col; ++j)
        {
            cin >> data;
            maze.initNode(i, j, data);
        }
    }
    maze.setNodeState();
    maze.searchMazePath();
    maze.showMazePath();
    cout << endl;
    return 1;
}

运行:

请输入迷宫的行列数,例如:10 10
5 5
请输入迷宫的路径信息:0表示可以走,1表示不能走
0 0 0 1 1
1 0 0 0 1
1 1 0 1 1
1 1 0 0 1
1 1 1 0 0
-------迷宫路径-------
* * * 1 1
1 0 * 0 1
1 1 * 1 1
1 1 * * 1
1 1 1 * *

再次运行一个不通的迷宫:

请输入迷宫的行列数,例如:10 10
5 5
请输入迷宫的路径信息:0表示可以走,1表示不能走
0 0 0 1 1
1 0 1 0 1
1 1 0 1 1
1 1 0 0 1
1 1 1 0 0
不存在一条迷宫路径

DFS会一直沿着预定的方向探测,比如上面探测方向的顺序是 右-下-左-上,如果其中右边一直能走,它会一直往右边,如果下边能走,就一直往下边。

比如:

请输入迷宫的行列数,例如:10 10
6 6
请输入迷宫的路径信息:0表示可以走,1表示不能走
0 0 1 1 1 1
1 0 0 0 0 1
1 0 1 1 0 1
1 0 0 0 0 1
1 0 1 1 1 1
1 0 0 0 0 0
-------迷宫路径-------
* * 1 1 1 1 
1 * * * * 1 
1 0 1 1 * 1 
1 * * * * 1 
1 * 1 1 1 1 
1 * * * * * 

显然,这不是最优路线。

三、广度优先搜索


广度优先搜索(Breadth First Search,BFS)则不同,它会"层层推进",先探索当前位置的所有相邻节点,再探索下一层节点,即BFS 按距离起点的步数分层扩展,它首先访问所有距离为 1 的节点,随后是距离为 2 的节点。

广度优先使用层层扩展的方式,依赖一个队列结构,在无权图中,BFS 首次到达终点的路径即为最短路径。其空间复杂度较高 O(V),需存储所有已访问节点。

实现代码如下:

#include <iostream>
#include <mutex>
#include <string>
#include <memory>
#include <queue>
#include <vector>
using namespace std;
 
class Maze
{
public:
    Maze(int c, int r) : col(c), row(r)
    {
        pMaze = new Node *[row];
        for (size_t i = 0; i < row; ++i)
        {
            pMaze[i] = new Node[col];
        }
        path.resize(row * col);
    }

    // 析构函数:释放内存
    ~Maze() {
      // 同上
    }

    void initNode(int x, int y, int val)
    {
       //同上
    }

    // 观察当前节点的上下走有四个方向的元素是否可以走,如果可以,修改状态。
    void setNodeState()
    {
         //同上
    }

    void searchMazePath()
    {
        if (pMaze[0][0].val == 1)
        {
            return;
        }
        queue.push(pMaze[0][0]);
        while (!queue.empty())
        {
            Node top = queue.front();
            int x = top.x;
            int y = top.y;
            // 已经找到右下角,得到迷宫路径
            if (x == row - 1 && y == col - 1)
            {
                return;
            }

            // 如果往右可以走
            if (pMaze[x][y].state[RIGHT])
            {
                pMaze[x][y].state[RIGHT] = false;
                pMaze[x][y + 1].state[LEFT] = false; // 只走一遍
                path[x * row + y + 1] = pMaze[x][y];   // 记录前驱节点(用于回溯路径)
                queue.push(pMaze[x][y + 1]);
                if (check(pMaze[x][y + 1]))
                {
                    return;
                }
            }

            // 如果往下可以走
            if (pMaze[x][y].state[DOWN])
            {
                pMaze[x][y].state[DOWN] = false;
                pMaze[x + 1][y].state[UP] = false; // 只走一遍
                path[(x + 1) * row + y] = pMaze[x][y];
                queue.push(pMaze[x + 1][y]);
                if (check(pMaze[x + 1][y]))
                {
                    return;
                }
            }

            // 如果往左可以走
            if (pMaze[x][y].state[LEFT])
            {
                pMaze[x][y].state[LEFT] = false;
                pMaze[x][y - 1].state[RIGHT] = false; // 只走一遍
                path[x * row + y - 1] = pMaze[x][y];
                queue.push(pMaze[x][y - 1]);
                if (check(pMaze[x][y - 1]))
                {
                    return;
                }
            }

            // 如果往上可以走
            if (pMaze[x][y].state[UP])
            {
                pMaze[x][y].state[UP] = false;
                pMaze[x - 1][y].state[DOWN] = false; // 只走一遍
                path[(x - 1) * row + y] = pMaze[x][y];
                queue.push(pMaze[x - 1][y]);
                if (check(pMaze[x - 1][y]))
                {
                    return;
                }
            }
            queue.pop();
        }
    }

    void showMazePath()
    {
        if (queue.empty())
        {
            cout << "不存在一条迷宫路径" << endl;
        }
        else
        {
            int x = row - 1;
            int y = col - 1;
            for (;;)
            {
                pMaze[x][y].val = '*';
                if (x == 0 && y == 0)
                {
                    break;
                }
                Node node = path[x * row + y];
                x = node.x;
                y = node.y;
            }

            for (size_t i = 0; i < row; ++i)
            {
                for (size_t j = 0; j < col; ++j)
                {
                    if (pMaze[i][j].val == '*')
                    {
                        cout << "* ";
                    }
                    else
                    {
                        cout << pMaze[i][j].val << " ";
                    }
                }
                cout << endl;
            }
        }
    }

private:
    struct Node
    {
        int x;
        int y;
        int val;
        bool state[WAY_NUM]; // 节点的前后左右4个方向是否可以走
    };
    bool check(Node &node)
    {
        return node.x == row - 1 && node.y == col - 1;
    }
    int row;
    int col;
    Node **pMaze;
    queue<Node> queue; // bfs依赖的队列结构
    vector<Node> path; // 记录bfs时,节点的行走信息
};

上述代码的核心逻辑为:

  • 队列的作用:队列用于保存待探索的节点,队首是当前处理的节点,处理完后出队,再将其所有可走的相邻节点入队。
  • 前驱节点记录:path数组保存每个节点的上一个节点(前驱),用于找到出口后回溯完整路径。
  • 层级扩展:BFS 会先处理完当前层的所有节点(同一距离的节点),再处理下一层,因此能保证找到最短路径。
  • 终止条件:只要发现某个节点是出口,立即返回,因为 BFS 的第一个出口路径就是最短路径。

调用方法如下:

int main()
{
    cout << "请输入迷宫的行列数,例如:10 10:";
    int row, col, data;
    cin >> row >> col;
    Maze maze(row, col); //
    cout << "请输入迷宫的路径信息(0表示可以走,1表示不能走)" << endl;
    for (size_t i = 0; i < row; ++i)
    {
        for (size_t j = 0; j < col; ++j)
        {
            cin >> data;
            maze.initNode(i, j, data);
        }
    }
    cout << "======================" << endl;
    maze.setNodeState();
    maze.searchMazePath();
    maze.showMazePath();
    cout << endl;
    return 1;
}

运行:

请输入迷宫的行列数,例如:10 10:6 6
请输入迷宫的路径信息(0表示可以走,1表示不能走)
0 0 1 1 1 1
1 0 0 0 0 1
1 0 1 1 0 1
1 0 0 0 0 1
1 0 1 1 1 1
1 0 0 0 0 0
======================
* * 1 1 1 1 
1 * 0 0 0 1 
1 * 1 1 0 1 
1 * 0 0 0 1 
1 * 1 1 1 1 
1 * * * * * 
请输入迷宫的行列数,例如:10 10:6 6
请输入迷宫的路径信息(0表示可以走,1表示不能走)
0 0 1 1 1 1
1 0 0 0 0 1
1 0 1 1 0 1
1 0 0 0 0 1
1 1 1 1 0 1
1 0 0 0 0 0
======================
* * 1 1 1 1 
1 * 0 0 0 1 
1 * 1 1 0 1 
1 * * * * 1 
1 1 1 1 * 1 
1 0 0 0 * * 

四、Dijkstra 算法


上述不管是DFS还是BFS,都是无权图即路径中每一步的代价都是相等的。DFS可以用来判断连通性,BFS可以用来解决每一步代价相同迷宫即无权图的最短路径。而Dijkstra算法可以认为是BFS的升级版,它可以解决 每一步代价 / 权重不一样 的即带权图最短路径问题(比如迷宫里有的路难走,代价是 2、3、5...)。

在实现上,由于每一步的权重不一样,所以队列结构方面不能再使用BFS中的普通的先进先出队列,而要使用优先级队列中的小根堆。可以认为BFS算法是Dijkstra算法再所有边权重=1时的特列Dijkstra是带权图的“高级BFS”

为了表示权重,需要在原有的Node节点上增加权重字段,并且需要提供一个函数对象及仿函数(Function Object)用来方便优先队列即小根堆来使用,以方便代价小的对象先出。

struct Node
{
    int x;
    int y;
    int val;             // 0=路 1=墙 * =路径
    int cost;            // 代价权重
};

struct Compare
{
    bool operator()(const Node &a, const Node &b)
    {
        return a.cost > b.cost; // 小根堆
    }
};

算法的代码实现如下:

#include <iostream>
#include <mutex>
#include <string>
#include <memory>
#include <queue>
#include <vector>
using namespace std;

const int RIGHT = 0;
const int DOWN = 1;
const int LEFT = 2;
const int UP = 3;
const int WAY_NUM = 4;

/*
0 0 1 1 1 1
1 0 0 0 0 1
1 0 1 1 0 1
1 0 0 0 0 1
1 0 1 1 1 1
1 0 0 0 0 0

0 0 1 1 1 1
1 0 0 0 0 1
1 0 1 1 0 1
1 0 0 0 0 1
1 1 1 1 0 1
1 0 0 0 0 0


0 1 1 1 1 1
1 1 0 0 0 1
1 0 1 1 0 1
1 0 0 0 0 1
1 1 1 1 0 1
1 0 0 0 0 0

*/

class Maze
{
public:
    Maze(int c, int r) : col(c), row(r)
    {
        pMaze = new Node *[row];
        for (size_t i = 0; i < row; ++i)
        {
            pMaze[i] = new Node[col];
        }
        path.resize(row * col);
        dist.resize(row, vector<int>(col, INT_MAX));
    }

    // 析构函数:释放内存
    ~Maze()
    {
        for (size_t i = 0; i < row; i++)
        {
            delete[] pMaze[i];
        }
        delete[] pMaze;
    }

    void initNode(int x, int y, int val, int cost = 1)
    {
        pMaze[x][y].x = x;
        pMaze[x][y].y = y;
        pMaze[x][y].val = val;
        pMaze[x][y].cost = cost;
    }
 
    void searchMazePath()
    {
        // 优先队列(小根堆)
        priority_queue<Node, vector<Node>, Compare> q;
        if (pMaze[0][0].val == 1)
        {
            return;
        }
        pMaze[0][0].cost = 0;
        dist[0][0] = 0;
        q.push(pMaze[0][0]);
        // 四个方向偏移量,右、下、左、上
        int dx[] = {1, 0, -1, 0};
        int dy[] = {0, 1, 0, -1};
        while (!q.empty())
        {
            Node top = q.top();
            q.pop();
            int x = top.x;
            int y = top.y;
            // 已经找到右下角,得到迷宫路径
            if (x == row - 1 && y == col - 1)
            {
                return;
            }

            // 旧代价,跳过
            if (dist[x][y] < top.cost)
            {
                continue;
            }

            for (int d = 0; d < WAY_NUM; d++)
            {
                int nx = x + dx[d];
                int ny = y + dy[d];

                if (nx < 0 || nx >= row || ny < 0 || ny >= col)
                {
                    continue; // 越界
                }

                if (pMaze[nx][ny].val == 1)
                {
                    continue; // 墙
                }

                // 新代价=当前代价+下一个节点的代价
                int newCost = dist[x][y] + pMaze[nx][ny].cost;

                if (newCost < dist[nx][ny])
                {
                    dist[nx][ny] = newCost;
                    path[nx * row + ny] = top; // 记录节点的行走信息
                    q.push(pMaze[nx][ny]);
                }
            }
        }
    }

    void showMazePath()
    {
        int x = row - 1;
        int y = col - 1;
        // 如果出口的前驱节点不存在 → 没路径

        if (dist[x][y] == INT_MAX)
        {
            cout << "Dijkstra:找不到迷宫路径!" << endl;
            return;
        }

        for (;;)
        {
            pMaze[x][y].val = '*';
            if (x == 0 && y == 0)
            {
                break;
            }
            Node node = path[x * row + y];
            x = node.x;
            y = node.y;
        }

        for (size_t i = 0; i < row; ++i)
        {
            for (size_t j = 0; j < col; ++j)
            {
                if (pMaze[i][j].val == '*')
                {
                    cout << "* ";
                }
                else
                {
                    cout << pMaze[i][j].val << " ";
                }
            }
            cout << endl;
        }
    }

private:
    struct Node
    {
        int x;
        int y;
        int val;             // 0=路 1=墙 * =路径
        int cost;            // 代价权重
    };

    struct Compare
    {
        bool operator()(const Node &a, const Node &b)
        {
            return a.cost > b.cost; // 小根堆
        }
    };

    bool check(Node &node)
    {
        return node.x == row - 1 && node.y == col - 1;
    }
    int row;
    int col;
    Node **pMaze;
    vector<vector<int>> dist; // 最小代价数组(Dijkstra核心)
    vector<Node> path;        // 记录bfs时,节点的行走信息
};

这里代码的核心逻辑在于:

  • 距离数组 dist 记录起点到每个点的最小代价
  • 优先队列(小根堆)priority_queue  每次选代价最小的点扩展
  • 用前驱数组 path 记录路径(与 BFS 完全一样)
  • 不能走回头路,只更新更短的路径

代码中,记录从起点到当前节点的代价变量是dist二维数组,它的起始值设置的是INT_MAX,这是一个技巧。

// 新代价=当前代价+下一个节点的代价
int newCost = dist[x][y] + pMaze[nx][ny].cost;
if (newCost < dist[nx][ny])
{
    dist[nx][ny] = newCost;
    path[nx * row + ny] = top; // 记录节点的行走信息
    q.push(pMaze[nx][ny]);
}
  • dist[x][y]表示从起点走到当前节点x,y的最小真实代价。这是已经确定的最优解
  • pMaze[nx][ny].cost是从当前节点,走到下一个节点nx,ny的代价。
  • newCost是从起点->当前节点->下一个接地那的总代价,也是新路线的代价
  • dist[nx][ny]是之前没有走过,或已经走过的从起点到下一个节点nx,ny的最小代价
  • 当第一次到达nx和ny时,一定会更新,因为它是无穷大
  • 如果之前通过其它路径到过nx和ny,如果当前的路线比这个代价更小,就更新为当前路径。

在DFS和BFS中,都需要判断State里面的方向来判断该方向是否能走。但在Dijkstra方法中,这些都以最小代价的方法来实现了。比如在BFS中,代码:

if (pMaze[x][y].state[RIGHT]) { ... }

判断向右边的状态,是否能否。然后如果可以走,还需要设置state的状态回false,防止走回头路。但在Dijkstra中,判断能不能走只看2个条件:

  1. 是不是墙,即val==1的不能走。
  2. 新路径的代价是否更小,即newCost<dist[nx][ny]。

只要不是墙,且路径更优就能走。

另外,在DFS和BFS中,防止走回头路的策略是,探测完一个方向之后,将该方向以及探测的方向的反方向都设置为false,防止走回头路,比如:

pMaze[x][y].state[RIGHT] = false;
pMaze[x][y+1].state[LEFT] = false;

但在Dijkstra中并不需要,原因有:

首先:dist数组自动拒绝更差的路径:

if (newCost < dist[nx][ny])

如果走回头路,新代价一定会大于原来的代码,上述不等式不成立。

其次:小根堆能保证一个点被弹出,就永远得到了最小代价,不可能再次被更新。

最后:即使被重复入队,也会被下面这句过滤:

if (dist[x][y] < u.cost) continue;

使用方法如下:

int main()
{
    cout << "请输入迷宫的行列数,例如:10 10:";
    int row, col, data;
    cin >> row >> col;
    Maze maze(row, col); //
    cout << "请输入迷宫的路径信息(0表示可以走,1表示不能走)" << endl;
    for (size_t i = 0; i < row; ++i)
    {
        for (size_t j = 0; j < col; ++j)
        {
            cin >> data;
            if ( data == 1)
            {
                 maze.initNode(i, j, data);//不能走,墙,权重为默认1
            }
            else
            {
                maze.initNode(i, j, 0, data);//可以走,否则权重为data
            }
        }
    }
    cout << "======================" << endl;
    maze.searchMazePath();
    maze.showMazePath();
    cout << endl;
    return 1;
}

输出结果如下:

请输入迷宫的行列数,例如:10 10:6 6
请输入迷宫的路径信息(0表示可以走,1表示不能走)
0 0 1 1 1 1
1 0 0 0 0 1
1 0 1 1 0 1
1 0 0 0 0 1
1 0 1 1 1 1
1 0 0 0 0 0
======================
* * 1 1 1 1 
1 * 0 0 0 1 
1 * 1 1 0 1 
1 * 0 0 0 1 
1 * 1 1 1 1 
1 * * * * * 

请输入迷宫的行列数,例如:10 10:6 6
请输入迷宫的路径信息(0表示可以走,1表示不能走)
0 0 1 1 1 1
1 0 0 0 0 1
1 0 1 1 0 1
1 0 0 0 0 1
1 1 1 1 0 1
1 0 0 0 0 0
======================
* * 1 1 1 1 
1 * 0 0 0 1 
1 * 1 1 0 1 
1 * * * * 1 
1 1 1 1 * 1 
1 0 0 0 * * 

可以看到,这里的路径跟BFS是完全一样的。

当我把下面这条路径的权重变大,它就会自动跳回到上一条路径:

请输入迷宫的行列数,例如:10 10:6 6
请输入迷宫的路径信息(0表示可以走,1表示不能走)
0 0 1 1 1 1
1 0 0 0 0 1
1 0 1 1 0 1
1 0 3 0 0 1
1 1 1 1 0 1
1 0 0 0 0 0
======================
* * 1 1 1 1 
1 * * * * 1 
1 0 1 1 * 1 
1 0 0 0 * 1 
1 1 1 1 * 1 
1 0 0 0 * * 

五、A*算法


如前所述,Dijkstra算法可以看作是有权版本的BFS,他们有一些缺点:

  • BFS和Dijkstra都是一层一层往外扩展搜索,类似水波纹,在很多搜索方向根本远离终点,浪费算力。
  • 在大的迷宫里,Dijkstra要跑遍半个地图才能找到重点,搜索的范围大,效率低。
  • Dijkstra只知道代价最小,他不知道目标终点的位置,没有“方向感”

为了解决Dijkstra算法的以上问题,A*算法应运而生,它既保留了最短路径的特性,又提高了搜索效率,它的做法是在原来的最小代价基础上,增加了启发函数(huristic fuction):

f(n) = g(n) + h(n)

上述公式中:

  • g(n):起点走到当前点的代价(= Dijkstra 的代价)
  • h(n):当前点 预估 到终点的代价(启发函数,A* 核心)
  • f(n):综合优先级,越小越优先走

在迷宫算法里,h(n)可以使用曼哈顿距离:

h = |x - endX| + |y - endY|

就是“横向差+纵向差”,非常快。

代码实现如下:

#include <iostream>
#include <mutex>
#include <string>
#include <memory>
#include <queue>
#include <vector>
using namespace std;

const int WAY_NUM = 4;

// 四个方向偏移量,右、下、左、上
const int dx[] = {1, 0, -1, 0};
const int dy[] = {0, 1, 0, -1};
 
class Maze
{
public:
    Maze(int c, int r) : col(c), row(r)
    {
        pMaze = new Node *[row];
        for (size_t i = 0; i < row; ++i)
        {
            pMaze[i] = new Node[col];
        }
        dist.resize(row, vector<int>(col, INT_MAX));
        path.resize(row, vector<Node>(col));
    }

    // 析构函数:释放内存
    ~Maze()
    {
        for (size_t i = 0; i < row; i++)
        {
            delete[] pMaze[i];
        }
        delete[] pMaze;
    }

    void initNode(int x, int y, int val, int cost = 1)
    {
        pMaze[x][y].x = x;
        pMaze[x][y].y = y;
        pMaze[x][y].val = val;
        pMaze[x][y].cost = cost;
    }

    // 曼哈顿距离(A*核心启发函数)
    int heuristic(int x, int y)
    {
        int ex = row - 1;
        int ey = col - 1;
        return abs(x - ex) + abs(y - ey);
    }

    void searchMazePath()
    {
        if (pMaze[0][0].val == 1)
        {
            return;
        }
        // 优先队列(小根堆)
        priority_queue<Node, vector<Node>, Compare> q;

        Node start = pMaze[0][0];
        start.g = 0;
        start.h = heuristic(0, 0);
        start.f = start.g + start.h;
        dist[0][0] = 0;
        q.push(start);

        while (!q.empty())
        {
            Node top = q.top();
            q.pop();

            int x = top.x;
            int y = top.y;
            // 已经找到右下角,得到迷宫路径
            if (x == row - 1 && y == col - 1)
            {
                return;
            }

            // 旧代价,跳过
            if (dist[x][y] < top.g)
            {
                continue;
            }

            for (int d = 0; d < WAY_NUM; d++)
            {
                int nx = x + dx[d];
                int ny = y + dy[d];

                if (nx < 0 || nx >= row || ny < 0 || ny >= col)
                {
                    continue; // 越界
                }

                if (pMaze[nx][ny].val == 1)
                {
                    continue; // 墙
                }

                // 新代价=当前代价+下一个节点的代价
                int newG = dist[x][y] + pMaze[nx][ny].cost;

                if (newG < dist[nx][ny])
                {
                    dist[nx][ny] = newG;
                    path[nx][ny] = top; // 记录节点的行走信息
                    Node nextNode = pMaze[nx][ny];
                    nextNode.g = newG;
                    nextNode.h = heuristic(nx, ny);
                    nextNode.f = nextNode.g + nextNode.h;
                    q.push(nextNode);
                }
            }
        }
    }

    void showMazePath()
    {
        int x = row - 1;
        int y = col - 1;
        // 如果出口的前驱节点不存在 → 没路径

        if (dist[x][y] == INT_MAX)
        {
            cout << "Dijkstra:找不到迷宫路径!" << endl;
            return;
        }

        for (;;)
        {
            pMaze[x][y].val = '*';
            if (x == 0 && y == 0)
            {
                break;
            }
            Node node = path[x][y];
            x = node.x;
            y = node.y;
        }

        for (size_t i = 0; i < row; ++i)
        {
            for (size_t j = 0; j < col; ++j)
            {
                if (pMaze[i][j].val == '*')
                {
                    cout << "* ";
                }
                else
                {
                    cout << pMaze[i][j].val << " ";
                }
            }
            cout << endl;
        }
    }

private:
    struct Node
    {
        int x;
        int y;
        int val;     // 0=路 1=墙 * =路径
        int cost;    // 代价(权重)
        int g, h, f; // g:已走代价 h:预估代价 f:总优先级
        Node(int x_, int y_, int val_, int cost_) : x(x_), y(y_), val(val_), cost(cost_) {}
        Node() : x(-1), y(-1), val(0), g(0), h(0), f(0) {}
    };

    // 小根堆:f值最小优先
    struct Compare
    {
        bool operator()(const Node &a, const Node &b)
        {
            return a.f > b.f;
        }
    };

    bool check(Node &node)
    {
        return node.x == row - 1 && node.y == col - 1;
    }
    int row;
    int col;
    Node **pMaze;
    vector<vector<int>> dist;  // 最小代价数组(Dijkstra核心)
    vector<vector<Node>> path; // 前驱节点
};

使用方法如下:

int main()
{
    cout << "请输入迷宫的行列数,例如:10 10:";
    int row, col, data;
    cin >> row >> col;
    Maze maze(row, col); //
    cout << "请输入迷宫的路径信息(0表示可以走,1表示不能走)" << endl;
    for (size_t i = 0; i < row; ++i)
    {
        for (size_t j = 0; j < col; ++j)
        {
            cin >> data;
            if (data == 1)
            {
                maze.initNode(i, j, data); // 不能走,墙,权重为默认1
            }
            else
            {
                maze.initNode(i, j, 0, data); // 可以走,否则权重为data
            }
        }
    }
    cout << "======================" << endl;
    maze.searchMazePath();
    maze.showMazePath();
    cout << endl;
    return 1;
}

六、四种算法的可视化对比


前面已经分别少了DFS、BFS、Dijkstra以及A Start算法以及各自的特点,下面这个程序可以动态显示各种算法之间的区别:

#include <iostream>
#include <queue>
#include <stack>
#include <vector>
#include <tuple>
#include <climits>
#include <cmath>
#include <windows.h>
using namespace std;

const int dx[] = { 0, 1, 0, -1 };
const int dy[] = { 1, 0, -1, 0 };

#define COL_DEFAULT    7
#define COL_VISITED    11
#define COL_PATH       10
#define COL_SAND       6
#define COL_MUD        4

struct Node {
    int x, y;
    int val;
    int cost;
    bool visited;
    bool isPath;

    Node()
        : x(-1), y(-1), val(1), cost(1), visited(false), isPath(false) {}

    Node(int x_, int y_, int v_, int c_)
        : x(x_), y(y_), val(v_), cost(c_), visited(false), isPath(false) {}
};

void setColor(int c) {
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), c);
}

class Maze {
public:
    int row, col;
    Node** pMaze;
    vector<vector<pair<int, int>>> prev;

    Maze(int r, int c) : row(r), col(c) {
        pMaze = new Node * [row];
        for (int i = 0; i < row; ++i)
            pMaze[i] = new Node[col];
        prev.resize(row, vector<pair<int, int>>(col, { -1,-1 }));
    }

    ~Maze() {
        for (int i = 0; i < row; ++i) delete[] pMaze[i];
        delete[] pMaze;
    }

    void setNode(int x, int y, int val, int cost) {
        pMaze[x][y] = Node(x, y, val, cost);
    }

    void showTip() {
        setColor(COL_DEFAULT);
        cout << "图例:";
        cout << "#=墙 ";
        cout << "O=平地 ";
        cout << "~=沙地 ";
        cout << "=泥地 ";
        setColor(COL_VISITED); cout << "蓝色=搜索过 ";
        setColor(COL_PATH); cout << "绿色=最终路径\n";
        setColor(COL_DEFAULT);
    }

    void draw(const string& title) {
        system("cls");
        cout << "===== " << title << " =====\n";
        showTip();

        for (int i = 0; i < row; ++i) {
            for (int j = 0; j < col; ++j) {
                char ch = '?';
                int color = COL_DEFAULT;

                if (pMaze[i][j].val == 1) {
                    ch = '#';
                    color = COL_DEFAULT;
                } else {
                    // 平地用 O,无编译错误,显示清晰
                    if (pMaze[i][j].cost == 1)
                        ch = 'O';
                    else if (pMaze[i][j].cost == 3)
                        ch = '~';
                    else if (pMaze[i][j].cost == 5)
                        ch = '=';

                    if (pMaze[i][j].isPath) {
                        color = COL_PATH;
                    } else if (pMaze[i][j].visited) {
                        color = COL_VISITED;
                    } else {
                        if (pMaze[i][j].cost == 3)
                            color = COL_SAND;
                        else if (pMaze[i][j].cost == 5)
                            color = COL_MUD;
                        else
                            color = COL_DEFAULT;
                    }
                }

                setColor(color);
                cout << ch << ' ';
            }
            cout << '\n';
        }
        setColor(COL_DEFAULT);
        Sleep(25);
    }

    void reset() {
        for (int i = 0; i < row; ++i) {
            for (int j = 0; j < col; ++j) {
                pMaze[i][j].visited = false;
                pMaze[i][j].isPath = false;
                prev[i][j] = { -1,-1 };
            }
        }
    }

    // DFS
    bool dfs(int x, int y) {
        if (x == row - 1 && y == col - 1) return true;
        if (pMaze[x][y].val == 1 || pMaze[x][y].visited) return false;

        pMaze[x][y].visited = true;
        draw("DFS 搜索中");

        for (int d = 0; d < 4; ++d) {
            int nx = x + dx[d];
            int ny = y + dy[d];
            if (nx >= 0 && ny >= 0 && nx < row && ny < col) {
                if (!pMaze[nx][ny].visited && pMaze[nx][ny].val == 0) {
                    prev[nx][ny] = { x, y };
                    if (dfs(nx, ny))
                        return true;
                }
            }
        }
        return false;
    }

    void dfsDrawPath() {
        int x = row - 1, y = col - 1;
        while (x != -1 && y != -1) {
            pMaze[x][y].isPath = true;
            auto p = prev[x][y];
            x = p.first;
            y = p.second;
        }
        draw("DFS 结果");
    }

    // BFS
    void bfs() {
        queue<pair<int, int>> q;
        vector<vector<pair<int, int>>> prev(row, vector<pair<int, int>>(col, { -1,-1 }));

        q.push({ 0,0 });
        pMaze[0][0].visited = true;

        while (!q.empty()) {
            auto [x, y] = q.front(); q.pop();
            draw("BFS 搜索中");

            if (x == row - 1 && y == col - 1) break;

            for (int d = 0; d < 4; ++d) {
                int nx = x + dx[d];
                int ny = y + dy[d];
                if (nx >= 0 && ny >= 0 && nx < row && ny < col) {
                    if (!pMaze[nx][ny].visited && pMaze[nx][ny].val == 0) {
                        pMaze[nx][ny].visited = true;
                        prev[nx][ny] = { x,y };
                        q.push({ nx,ny });
                    }
                }
            }
        }

        int x = row - 1, y = col - 1;
        while (x != -1) {
            pMaze[x][y].isPath = true;
            auto p = prev[x][y];
            x = p.first; y = p.second;
        }
        draw("BFS 结果");
    }

    // Dijkstra
    void dijkstra() {
        using Tuple = tuple<int, int, int>;
        priority_queue<Tuple, vector<Tuple>, greater<>> q;
        vector<vector<int>> dist(row, vector<int>(col, INT_MAX));
        vector<vector<pair<int, int>>> prev(row, vector<pair<int, int>>(col, { -1,-1 }));

        dist[0][0] = 0;
        q.emplace(0, 0, 0);

        while (!q.empty()) {
            auto [c, x, y] = q.top(); q.pop();
            if (pMaze[x][y].visited) continue;
            pMaze[x][y].visited = true;
            draw("Dijkstra 搜索中");

            if (x == row - 1 && y == col - 1) break;

            for (int d = 0; d < 4; ++d) {
                int nx = x + dx[d];
                int ny = y + dy[d];
                if (nx >= 0 && ny >= 0 && nx < row && ny < col && pMaze[nx][ny].val == 0) {
                    int newCost = c + pMaze[nx][ny].cost;
                    if (newCost < dist[nx][ny]) {
                        dist[nx][ny] = newCost;
                        prev[nx][ny] = { x,y };
                        q.emplace(newCost, nx, ny);
                    }
                }
            }
        }

        int x = row - 1, y = col - 1;
        while (x != -1) {
            pMaze[x][y].isPath = true;
            auto p = prev[x][y];
            x = p.first; y = p.second;
        }
        draw("Dijkstra 结果");
    }

    // A*
    void astar() {
        using Tuple = tuple<int, int, int, int>;
        priority_queue<Tuple, vector<Tuple>, greater<>> q;
        vector<vector<int>> dist(row, vector<int>(col, INT_MAX));
        vector<vector<pair<int, int>>> prev(row, vector<pair<int, int>>(col, { -1,-1 }));

        auto h = [&](int x, int y) {
            return abs(x - (row - 1)) + abs(y - (col - 1));
        };

        dist[0][0] = 0;
        q.emplace(h(0, 0), 0, 0, 0);

        while (!q.empty()) {
            auto [f, g, x, y] = q.top(); q.pop();
            if (pMaze[x][y].visited) continue;
            pMaze[x][y].visited = true;
            draw("A* 搜索中");

            if (x == row - 1 && y == col - 1) break;

            for (int d = 0; d < 4; ++d) {
                int nx = x + dx[d];
                int ny = y + dy[d];
                if (nx >= 0 && ny >= 0 && nx < row && ny < col && pMaze[nx][ny].val == 0) {
                    int newG = g + pMaze[nx][ny].cost;
                    if (newG < dist[nx][ny]) {
                        dist[nx][ny] = newG;
                        prev[nx][ny] = { x,y };
                        q.emplace(newG + h(nx, ny), newG, nx, ny);
                    }
                }
            }
        }

        int x = row - 1, y = col - 1;
        while (x != -1) {
            pMaze[x][y].isPath = true;
            auto p = prev[x][y];
            x = p.first; y = p.second;
        }
        draw("A* 结果");
    }
};

int map[16][16] = {
    {0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
    {1,0,0,0,0,0,0,0,2,2,0,0,0,0,0,1},
    {1,0,1,1,1,1,0,1,3,3,0,1,1,1,0,1},
    {1,0,1,0,0,0,0,1,3,3,0,0,0,1,0,1},
    {1,0,1,0,1,1,1,1,0,0,1,1,0,1,0,1},
    {1,0,1,0,1,0,0,0,0,1,0,0,0,1,0,1},
    {1,0,1,0,1,0,1,1,1,1,1,1,0,1,0,1},
    {1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,1},
    {1,1,1,0,1,1,1,1,1,1,0,1,0,1,1,1},
    {1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1},
    {1,0,1,1,1,1,1,1,0,1,1,1,1,1,0,1},
    {1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1},
    {1,1,1,1,1,1,0,1,1,1,1,1,1,1,0,1},
    {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
    {1,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0},
    {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0},
};

int main() {
    Maze maze(16, 16);

    for (int i = 0; i < 16; ++i) {
        for (int j = 0; j < 16; ++j) {
            if (map[i][j] == 1)
                maze.setNode(i, j, 1, 0);
            else if (map[i][j] == 2)
                maze.setNode(i, j, 0, 3);
            else if (map[i][j] == 3)
                maze.setNode(i, j, 0, 5);
            else
                maze.setNode(i, j, 0, 1);
        }
    }

    cout << "16×16 迷宫寻路对比\n";
    cout << "依次演示:DFS → BFS → Dijkstra → A*\n按回车开始...";
    cin.get();

    maze.reset();
    maze.dfs(0, 0);
    maze.dfsDrawPath();
    cout << "\nDFS 结束,回车继续 BFS..."; cin.get();

    maze.reset();
    maze.bfs();
    cout << "\nBFS 结束,回车继续 Dijkstra..."; cin.get();

    maze.reset();
    maze.dijkstra();
    cout << "\nDijkstra 结束,回车继续 A*..."; cin.get();

    maze.reset();
    maze.astar();

    cout << "\n全部演示完成!\n";
    return 0;
}

下面这个是DFS的输出结果:

BFS的结果:

Dijkstra的结果:

A Star的结果:

下面是对这几种算法的简单比较: