【C++】从迷宫游戏看懂 DFS 与 BFS:原理、实现与应用场景

在数据结构与算法学习中,深度优先搜索(DFS)和广度优先搜索(BFS)是两种最基础也最常用的图遍历算法。它们不仅是面试高频考点,更是解决路径规划、拓扑排序、连通性分析等问题的核心工具。本文将通过一个完整的 C++ 迷宫游戏实例,带大家深入理解 DFS 与 BFS 的工作原理、代码实现及适用场景,让抽象的算法变得直观可感。

观前提示:本文使用AI写作

一、迷宫游戏整体介绍

首先,我们先对这个 C++ 迷宫游戏做一个整体了解,明确其功能与核心模块,为后续分析 DFS 和 BFS 的应用做铺垫。

1.1 游戏核心功能

  • 随机迷宫生成:通过算法自动生成有通路的迷宫(墙与路径分布随机)
  • 玩家交互:支持方向键 / WASD 控制玩家移动,ESC 键退出游戏
  • 路径验证:确保生成的迷宫存在从起点(S)到终点(E)的通路
  • 最短路径计算:预先计算起点到终点的最短步数,用于最终得分评估
  • 游戏结算:到达终点后显示玩家步数、最短步数及最终得分

1.2 核心数据结构

// 迷宫单元格状态(枚举)
enum Cell { WALL = 0, PATH = 1, START = 2, END = 3, PLAYER = 4 };

// 坐标结构(存储迷宫中任意点的位置)
struct Point { int x, y; Point(int x = 0, int y = 0) : x(x), y(y) {} };

// 迷宫类核心成员
class MazeGame {
private:
    vector<vector<int>> maze;  // 二维向量存储迷宫数据(WIDTH列×HEIGHT行)
    Point start, end, player;  // 起点、终点、玩家位置
    int steps;                 // 玩家已走步数
    int minSteps;              // 最短路径步数
    // ... 成员函数
};

1.3 游戏运行流程

  1. 初始化迷宫:调用generateMaze()生成随机迷宫
  2. 路径验证:调用hasPath()确保迷宫有解(起点到终点连通)
  3. 计算最短路径:调用calculateMinSteps()获取最短步数
  4. 进入主循环:run()函数处理按键输入、更新玩家位置、刷新界面
  5. 游戏结束:玩家到达终点后计算得分并显示结果

二、深度优先搜索(DFS):迷宫生成与路径验证

深度优先搜索(Depth-First Search,简称 DFS)的核心思想是 “一条路走到黑”:从起点出发,优先探索当前路径的最深节点,若遇到死胡同则回溯,直到找到目标或遍历所有节点。其典型实现依赖栈(Stack) 或递归,本文实例中使用栈实现。

2.1 DFS 的应用场景 1:随机迷宫生成

迷宫生成的核心需求是 “创建连通的路径网络,同时保留足够的墙”。这里使用DFS 迷宫生成算法(也叫 “递归回溯法”),步骤如下:
算法原理

  1. 初始化迷宫:所有单元格设为墙(WALL)
  2. 选择起点:通常选迷宫内部点(避免在边缘),设为路径(PATH)并压入栈
  3. 栈非空时循环:
    • 弹出栈顶节点(当前探索点)
    • 随机打乱 4 个方向(上、下、左、右),避免迷宫模式固定
    • 对每个方向,计算 “两步外” 的节点(避免路径过密):
      • 若两步外节点在迷宫内部且为墙:
      • 将两步外节点设为路径
      • 将当前节点与两步外节点之间的墙设为路径(打通通道)
      • 重新将当前节点压入栈(后续可能继续探索其他方向)
      • 将两步外节点压入栈(继续探索新路径)
      • 跳出当前方向循环,进入新节点探索

核心代码实现

void generateMaze() {
    // 1. 初始化全部为墙
    maze = vector<vector<int>>(HEIGHT, vector<int>(WIDTH, WALL));
    
    // 2. 设定起点(1,1)和终点(HEIGHT-2, WIDTH-2)(边缘留1层墙)
    start = Point(1, 1);
    end = Point(HEIGHT - 2, WIDTH - 2);
    
    // 3. DFS栈初始化:起点压栈,设为路径
    stack<Point> s;
    s.push(start);
    maze[start.x][start.y] = PATH;
    
    while (!s.empty()) {
        Point current = s.top();
        s.pop();
        
        // 4. 随机打乱方向(0-上,1-下,2-左,3-右)
        vector<int> directions = {0, 1, 2, 3};
        for (int i = 0; i < 4; ++i) {
            swap(directions[i], directions[rand() % 4]);
        }
        
        // 5. 探索每个方向
        for (int dir : directions) {
            // 计算两步外的节点(避免路径相邻)
            int nx = current.x + dx[dir] * 2;
            int ny = current.y + dy[dir] * 2;
            
            // 检查两步外节点是否合法(在内部且为墙)
            if (nx > 0 && nx < HEIGHT - 1 && ny > 0 && ny < WIDTH - 1 && maze[nx][ny] == WALL) {
                // 打通通道:两步外节点和中间节点设为路径
                maze[nx][ny] = PATH;
                maze[current.x + dx[dir]][current.y + dy[dir]] = PATH;
                
                // 回溯准备:当前节点重新压栈,新节点压栈
                s.push(current);
                s.push(Point(nx, ny));
                break;  // 优先探索新路径,下次循环处理其他方向
            }
        }
    }
    
    // 标记起点和终点
    maze[start.x][start.y] = START;
    maze[end.x][end.y] = END;
    player = start;  // 玩家初始位置在起点
}

为什么用 DFS 生成迷宫?
DFS 生成的迷宫具有 “狭长、多分支” 的特点,路径更具探索性(类似迷宫游戏的体验)。相比其他算法(如 Prim 算法),DFS 实现简单且能保证迷宫连通性,非常适合小型迷宫场景。

2.2 DFS 的应用场景 2:迷宫路径验证

生成迷宫后,需要确保存在从起点到终点的通路(避免玩家无法到达终点)。这里使用 DFS 判断迷宫的连通性,核心是遍历所有可达节点,检查是否包含终点
算法原理

  1. 初始化 visited 数组:标记节点是否已访问(避免重复遍历)
  2. 起点压栈,标记为已访问
  3. 栈非空时循环:
    • 弹出栈顶节点
    • 若为终点,返回 true(存在通路)
    • 遍历 4 个方向,若相邻节点合法(在迷宫内部、未访问、非墙):
      • 标记为已访问,压入栈
  4. 栈空时返回 false(无通路)

核心代码实现

bool hasPath() {
    // 初始化访问标记数组
    vector<vector<bool>> visited(HEIGHT, vector<bool>(WIDTH, false));
    stack<Point> s;
    s.push(start);
    visited[start.x][start.y] = true;
    
    while (!s.empty()) {
        Point current = s.top();
        s.pop();
        
        // 找到终点,返回有通路
        if (current.x == end.x && current.y == end.y) {
            return true;
        }
        
        // 探索4个方向
        for (int i = 0; i < 4; ++i) {
            int nx = current.x + dx[i];
            int ny = current.y + dy[i];
            
            // 检查节点合法性:在范围内、未访问、非墙
            if (nx >= 0 && nx < HEIGHT && ny >= 0 && ny < WIDTH && 
                !visited[nx][ny] && maze[nx][ny] != WALL) {
                visited[nx][ny] = true;
                s.push(Point(nx, ny));
            }
        }
    }
    
    // 遍历完所有可达节点,未找到终点
    return false;
}

2.3 DFS 的核心特点与适用场景

特点说明
实现方式栈(迭代)或递归(函数调用栈本质也是栈)
遍历顺序深度优先,优先探索当前路径的最深节点
空间复杂度O (V)(V 为节点数,栈最多存储所有节点)
最优解不保证找到最优解(可能陷入局部路径)
适用场景迷宫生成、连通性判断、拓扑排序、深度优先遍历(如二叉树前 / 中 / 后序)

注意:递归实现 DFS 时需注意栈溢出问题(如迷宫过大时,递归深度过深会导致栈溢出),因此本文采用迭代(栈)实现更安全。

三、广度优先搜索(BFS):最短路径计算

广度优先搜索(Breadth-First Search,简称 BFS)的核心思想是 **“逐层扩散”:从起点出发,先遍历所有距离为 1 的节点,再遍历距离为 2 的节点,直到找到目标或遍历所有节点。其典型实现依赖队列(Queue),且能保证找到最短路径 **(在无权图中)。

3.1 BFS 的应用场景:迷宫最短路径计算

游戏需要预先计算起点到终点的最短步数,用于最终得分评估(玩家步数越接近最短步数,得分越高)。BFS 是解决 “无权图最短路径” 的最优选择。
算法原理

  1. 初始化 dist 数组:存储起点到每个节点的最短距离(-1 表示未访问)
  2. 起点入队,dist [起点] 设为 0(距离为 0)
  3. 队列非空时循环:
    • 出队队首节点
    • 若为终点,记录 dist 值并返回(即最短步数)
    • 遍历 4 个方向,若相邻节点合法(在迷宫内部、未访问、非墙):
      • dist [相邻节点] = dist [当前节点] + 1(距离 + 1)
      • 相邻节点入队
  4. 队列空时表示无通路(本文中已通过 hasPath () 确保有通路,此情况不会发生)

核心代码实现

void calculateMinSteps() {
    // 初始化距离数组:-1表示未访问
    vector<vector<int>> dist(HEIGHT, vector<int>(WIDTH, -1));
    queue<Point> q;
    
    q.push(start);
    dist[start.x][start.y] = 0;  // 起点距离为0
    
    while (!q.empty()) {
        Point current = q.front();
        q.pop();
        
        // 找到终点,记录最短距离并返回
        if (current.x == end.x && current.y == end.y) {
            minSteps = dist[current.x][current.y];
            return;
        }
        
        // 探索4个方向
        for (int i = 0; i < 4; ++i) {
            int nx = current.x + dx[i];
            int ny = current.y + dy[i];
            
            // 检查节点合法性:在范围内、未访问、非墙
            if (nx >= 0 && nx < HEIGHT && ny >= 0 && ny < WIDTH && 
                dist[nx][ny] == -1 && maze[nx][ny] != WALL) {
                dist[nx][ny] = dist[current.x][current.y] + 1;  // 距离+1
                q.push(Point(nx, ny));
            }
        }
    }
}

为什么 BFS 能找到最短路径?
在无权图中(迷宫的路径权重均为 1),BFS 的 “逐层扩散” 特性确保:第一次到达终点时,所经过的路径一定是最短的。例如,距离起点为 3 的节点一定在距离为 2 的节点之后被遍历,因此不会出现 “绕远路” 的情况。而 DFS 无法保证这一点,可能会先探索到一条较长的路径。

3.2 BFS 的核心特点与适用场景

特点说明
实现方式队列(FIFO,先进先出)
遍历顺序广度优先,逐层扩散,先访问近节点再访问远节点
空间复杂度O (V)(队列最多存储所有节点,在 “完全图” 场景下空间消耗可能比 DFS 大)
最优解保证找到无权图的最短路径
适用场景最短路径计算(如迷宫、地图导航)、层次遍历(如二叉树层序遍历)、连通性判断

四、DFS 与 BFS 的核心区别对比

通过迷宫游戏的实例,我们可以清晰地看到 DFS 与 BFS 的差异。下表从多个维度对两者进行总结:

对比维度深度优先搜索(DFS)广度优先搜索(BFS)
数据结构栈(Stack)或递归栈队列(Queue)
遍历顺序深度优先,“一条路走到黑”广度优先,“逐层扩散”
空间复杂度O (V)(最坏情况),递归可能栈溢出O (V)(最坏情况),无栈溢出问题
最短路径不保证找到最优解保证找到无权图的最短路径
迷宫生成效果路径狭长、多分支,探索性强路径均匀、分支少,不适合生成迷宫
适用场景迷宫生成、拓扑排序、深度遍历最短路径、层次遍历、邻居查找
访问标记需 visited 数组避免重复遍历需 visited 数组或 dist 数组避免重复遍历

五、游戏完整运行与体验

5.1 编译与运行

该代码基于 C++ 标准库编写,依赖<conio.h>(按键输入)和<windows.h>(清屏),适合在 Windows 环境下使用:

  1. 将代码保存为MazeGame.cpp
  2. 使用 MinGW 或 Visual Studio 编译:g++ MazeGame.cpp -o MazeGame.exe
  3. 运行MazeGame.exe,即可开始游戏

5.2 游戏操作

  • 移动:方向键(上 / 下 / 左 / 右)或 WASD 键(W 上 / S 下 / A 左 / D 右)
  • 退出:ESC 键
  • 得分规则:步数越接近最短步数,得分越高(最短步数得 100 分,每多 10% 步数减 10 分,最低 10 分)

六、总结与扩展

通过这个迷宫游戏实例,我们不仅掌握了 DFS 和 BFS 的实现细节,更理解了它们的 “应用场景差异”——DFS 适合 “探索性” 任务(如迷宫生成),BFS 适合 “最优路径” 任务(如最短路径计算)。

6.1 算法扩展

  • 加权图最短路径:若迷宫路径有权重(如某些路径需要消耗更多步数),BFS 不再适用,需使用 Dijkstra 算法或 A * 算法
  • 迷宫优化:可增加 “地图缩放”“障碍物动态生成” 等功能,进一步锻炼图算法的应用能力
  • 可视化增强:可使用 SFML 或 OpenGL 替换控制台输出,实现更精美的迷宫图形界面

6.2 学习建议

  1. 手动模拟 DFS 和 BFS 的遍历过程(如在纸上画一个小迷宫,模拟栈 / 队列的变化)
  2. 尝试用递归实现 DFS(注意栈溢出问题)
  3. 基于本文代码,添加 “显示最短路径” 功能(BFS 计算时记录路径,游戏中用特殊符号标记)

希望本文能帮助你从 “代码实现” 到 “原理理解” 再到 “场景应用”,全面掌握 DFS 和 BFS 这两个基础算法。算法学习的核心在于 “举一反三”,后续可尝试用这两种算法解决更多问题(如岛屿数量、最大路径和等),逐步提升算法思维能力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值