神秘迷宫探险 - 详细题解教程
神秘迷宫探险 - 详细题解教程
一、问题描述
K小姐发现了一座神秘迷宫,需要从入口到达宝藏出口。迷宫具有以下特点:
-
基本元素:
0
: 可通行的道路1
: 不可穿越的墙壁S
: 入口E
: 宝藏出口2
: 传送门(成对出现)
-
移动规则:
- 只能向上、下、左、右四个方向移动
- 不能走出边界或穿越墙壁
- 传送门可瞬间传送到配对的另一个传送门,不消耗步数
-
目标: 求从入口到出口的最短路径步数,无法到达则输出
-1
二、算法思路
2.1 核心算法: BFS(广度优先搜索)
为什么选择BFS?
- BFS天然保证第一次到达目标点时就是最短路径
- 适合在无权图或边权相同的图中求最短路径
- 时间复杂度O(m×n),适合题目数据范围
2.2 传送门处理机制
传送门是本题的核心难点:
-
配对规则: 传送门按顺序两两配对
- 第1个和第2个配对
- 第3个和第4个配对
- 以此类推
-
传送特性:
- 传送不消耗步数
- 从传送门A到传送门B,步数保持不变
-
实现方法: 使用哈希表建立传送门之间的双向映射
三、算法步骤详解
步骤1: 地图解析
读取迷宫 → 找到S位置 → 找到E位置 → 收集所有2的位置
步骤2: 传送门配对
遍历传送门数组,每两个建立双向映射关系
步骤3: BFS搜索
初始化队列,加入起点(x, y, steps=0)
↓
取出队首元素
↓
检查是否到达终点 → 是: 返回步数
↓ 否: 继续
尝试4个方向移动
↓
如果当前位置是传送门,尝试传送
↓
将新位置加入队列
↓
重复直到队列为空
四、完整代码实现
#include <bits/stdc++.h> // 万能头文件,包含所有标准库(STL容器、算法、IO等)
using namespace std; // 使用标准命名空间,避免写std::前缀int main() {// 关闭C++流与C标准流的同步,提升输入输出速度ios::sync_with_stdio(false);// 解除cin与cout的绑定,进一步加速输入(但不能混用scanf/printf)cin.tie(nullptr);// 声明迷宫的行数m和列数nint m, n;// 从标准输入读取迷宫尺寸cin >> m >> n;// ==================== 第一步: 读取迷宫地图 ====================// 创建一个大小为m的字符串向量,用于存储迷宫的每一行vector<string> maze(m);// 循环读取m行迷宫数据for (int i = 0; i < m; i++) {// 读取第i行的字符串(包含n个字符: 0,1,2,S,E)cin >> maze[i];}// ==================== 第二步: 定位关键位置 ====================// 声明起点和终点的坐标,用pair<行,列>表示pair<int, int> start, end;// 创建向量存储所有传送门的坐标vector<pair<int, int>> portals;// 双层循环遍历整个迷宫,找出S、E和所有2的位置for (int i = 0; i < m; i++) { // 遍历每一行for (int j = 0; j < n; j++) { // 遍历每一列// 如果当前位置是起点'S'if (maze[i][j] == 'S') {// 记录起点坐标为(i, j)start = {i, j};} // 如果当前位置是终点'E'else if (maze[i][j] == 'E') {// 记录终点坐标为(i, j)end = {i, j};} // 如果当前位置是传送门'2'else if (maze[i][j] == '2') {// 将传送门坐标添加到列表中portals.push_back({i, j});}}}// ==================== 第三步: 建立传送门配对关系 ====================// 创建映射表: key是一个传送门坐标,value是它配对的另一个传送门坐标map<pair<int, int>, pair<int, int>> portalMap;// 每次步进2,将传送门两两配对 (第0和第1配对,第2和第3配对...)for (int k = 0; k + 1 < portals.size(); k += 2) {// 建立第k个传送门到第k+1个传送门的映射 (A → B)portalMap[portals[k]] = portals[k + 1];// 建立第k+1个传送门到第k个传送门的映射 (B → A) 双向映射portalMap[portals[k + 1]] = portals[k];}// ==================== 第四步: BFS广度优先搜索 ====================// 创建BFS队列,每个元素是array<int,3>存储 {x坐标, y坐标, 当前步数}queue<array<int, 3>> q;// 创建访问标记集合,记录已经访问过的坐标,防止重复访问set<pair<int, int>> visited;// 将起点加入队列,初始步数为0q.push({start.first, start.second, 0});// 标记起点已访问visited.insert(start);// 定义四个方向的偏移量: 上(-1,0) 下(1,0) 左(0,-1) 右(0,1)int dx[] = {-1, 1, 0, 0}; // x方向(行)的偏移int dy[] = {0, 0, -1, 1}; // y方向(列)的偏移// BFS主循环: 当队列不为空时持续搜索while (!q.empty()) {// 取出队首元素,使用结构化绑定自动解包为x, y, stepsauto [x, y, steps] = q.front();// 弹出队首元素q.pop();// -------------------- 检查是否到达终点 --------------------// 如果当前坐标等于终点坐标if (make_pair(x, y) == end) {// 输出最短步数并结束程序cout << steps << "\n";return 0;}// -------------------- 尝试向四个方向移动 --------------------// 遍历四个方向(上、下、左、右)for (int d = 0; d < 4; d++) {// 计算新位置的x坐标 (当前x + 方向偏移)int nx = x + dx[d];// 计算新位置的y坐标 (当前y + 方向偏移)int ny = y + dy[d];// 检查新位置是否合法:// 1. nx >= 0 && nx < m: x坐标在有效范围内// 2. ny >= 0 && ny < n: y坐标在有效范围内// 3. maze[nx][ny] != '1': 不是墙壁// 4. visited.find({nx, ny}) == visited.end(): 未被访问过if (nx >= 0 && nx < m && ny >= 0 && ny < n && maze[nx][ny] != '1' && visited.find({nx, ny}) == visited.end()) {// 标记新位置已访问 (必须在入队前标记,防止重复入队)visited.insert({nx, ny});// 将新位置加入队列,步数+1 (普通移动消耗1步)q.push({nx, ny, steps + 1});}}// -------------------- 处理传送门传送 --------------------// 将当前坐标构造为pair用于查询mapauto currentPos = make_pair(x, y);// 检查当前位置是否是传送门 (在portalMap中能找到对应项)if (portalMap.count(currentPos)) {// 获取配对传送门的坐标,使用结构化绑定解包为tx, tyauto [tx, ty] = portalMap[currentPos];// 检查传送目标位置是否未被访问过if (visited.find({tx, ty}) == visited.end()) {// 标记传送目标位置已访问visited.insert({tx, ty});// 将传送目标位置加入队列,步数不变! (传送不消耗步数)q.push({tx, ty, steps});}}}// ==================== 第五步: 无法到达终点 ====================// 如果BFS结束后仍未找到终点,说明无路径可达cout << -1 << "\n";return 0; // 程序正常结束
}
五、代码详解
5.1 数据结构选择
数据结构 | 用途 | 原因 |
---|---|---|
vector<string> | 存储迷宫 | 方便按行读取和索引 |
pair<int,int> | 存储坐标 | 轻量级坐标表示 |
vector<pair<int,int>> | 存储传送门列表 | 动态收集所有传送门 |
map<pair,pair> | 传送门映射 | 快速查找配对传送门 |
queue<array<int,3>> | BFS队列 | 存储{x,y,步数} |
set<pair<int,int>> | 访问标记 | 防止重复访问 |
5.2 关键代码解析
1. 传送门配对
for (int k = 0; k + 1 < portals.size(); k += 2) {portalMap[portals[k]] = portals[k + 1]; // A → BportalMap[portals[k + 1]] = portals[k]; // B → A
}
- 每次跳过2个建立双向映射
- 确保从任一传送门都能找到配对门
2. BFS状态管理
auto [x, y, steps] = q.front(); // C++17结构化绑定
- 优雅地解包队列元素
3. 边界和有效性检查
if (nx >= 0 && nx < m && ny >= 0 && ny < n && // 边界maze[nx][ny] != '1' && // 非墙壁visited.find({nx, ny}) == visited.end()) // 未访问
4. 传送门传送逻辑
q.push({tx, ty, steps}); // 注意:steps不加1!
六、样例分析
样例1分析
输入:
5 5
S0000
11110
01010
01010
0000E迷宫可视化:
S 0 0 0 0
1 1 1 1 0
0 1 0 1 0
0 1 0 1 0
0 0 0 0 E路径: S → 右4步到(0,4) → 下4步到(4,4)=E
总步数: 8
样例2分析
输入:
3 3
S00
111
E00迷宫可视化:
S 0 0
1 1 1
E 0 0分析: S和E被1完全隔开,无路径
输出: -1
七、复杂度分析
时间复杂度: O(m×n)
- 每个格子最多被访问一次
- 传送门查找O(1)
- 总操作次数: O(m×n)
空间复杂度: O(m×n)
- 迷宫存储: O(m×n)
- 访问集合: O(m×n)
- 队列最大: O(m×n)
- 传送门映射: O(传送门数量)
八、注意事项与易错点
⚠️ 易错点1: 传送门步数处理
// ❌ 错误: 传送也加步数
q.push({tx, ty, steps + 1});// ✅ 正确: 传送不加步数
q.push({tx, ty, steps});
⚠️ 易错点2: 传送门配对
// ❌ 错误: 只建立单向映射
portalMap[portals[k]] = portals[k + 1];// ✅ 正确: 建立双向映射
portalMap[portals[k]] = portals[k + 1];
portalMap[portals[k + 1]] = portals[k];
⚠️ 易错点3: 访问标记时机
// ❌ 错误: 出队时标记(可能重复入队)
if (visited.find({nx, ny}) == visited.end()) {q.push({nx, ny, steps + 1});visited.insert({nx, ny}); // 太晚了!
}// ✅ 正确: 入队前立即标记
visited.insert({nx, ny});
q.push({nx, ny, steps + 1});
九、扩展思考
扩展1: 如果传送消耗步数怎么办?
修改传送逻辑:
q.push({tx, ty, steps + 传送消耗});
扩展2: 如果有多个终点?
set<pair<int, int>> endpoints; // 存储所有终点
// BFS时检查是否到达任一终点
if (endpoints.count({x, y})) { ... }
扩展3: 如果要输出路径?
使用parent数组记录路径:
map<pair<int,int>, pair<int,int>> parent;
// 找到终点后回溯构建路径
十、总结
本题是BFS最短路径的经典应用,关键点:
✅ BFS保证最短路径
✅ 传送门不消耗步数
✅ 双向映射处理配对关系
✅ visited集合防止重复访问
掌握这道题,你就掌握了:
- BFS框架应用
- 特殊移动规则处理
- 图论最短路径思想