# [牛客网 235903] 孙悟空救师傅 题解
# 📋 题目概述
孙悟空需要在一个 n×n 的网格迷宫中救出师傅。网格中包含不同的房间类型:
| 字符 | 含义 | 备注 |
|---|
K | 孙悟空起点 | 保证有且仅有一个 |
T | 师傅终点 | 保证有且仅有一个 |
S | 有蛇的房间 | 最多 5 个,需额外时间 |
# | 墙 | 不可通过 |
. | 空房间 | 正常通过 |
1-m | 钥匙 | 必须按顺序收集 |
# 🎯 核心约束
移动耗时:
- 普通移动:1 分钟 / 格
- 遇到蛇房间:首次进入需额外 1 分钟(共 2 分钟)
- 打倒蛇后,该房间变为普通房间
钥匙收集顺序:
- 必须按
1→2→...→m 的顺序收集 - 收集第
i 种钥匙前,必须已收集前 i-1 种 - 每种钥匙至少收集一把
救出条件:
# 🧠 算法设计
# 1. 状态建模
这是典型的 状态空间搜索 问题。由于钥匙收集有顺序要求,且蛇房间状态会改变,我们需要维护多维状态:
状态定义:
1 2 3
| state = (x, y, k) - (x, y): 当前位置坐标 - k: 已收集的钥匙种类数 (0 ≤ k ≤ m)
|
# 2. 状态转移图
1 2 3 4 5 6 7 8
| 普通移动 (+1min) state ──────────────→ 新位置 │ │ 遇到蛇房间 (+2min) │ (并标记蛇已被打倒) ↓ 收集钥匙 (需 k+1) state ──────────────→ k增加
|
# 3. 算法选择
由于边权有 1 和 2 两种,通常应使用 Dijkstra 或 01-BFS。但本题特殊之处在于:
- 只有首次进入蛇房间是 2 分钟,之后都是 1 分钟
- 可以修改地图来记录蛇的状态变化
因此采用 BFS + 状态标记 即可,因为每次扩展时,我们只移动到相邻单元格,且通过标记蛇房间可以保证后续处理正确。
# 4. 关键处理技巧
# (1) 蛇房间的状态处理
1 2 3 4 5 6 7 8 9 10
| if(g[nx][ny] == -2) { g[nx][ny] = -1; time_cost = 2; }
else { time_cost = 1; }
|
这样处理后,
不需要额外的状态位记录蛇的位置,大大简化了状态空间。
# (2) 钥匙收集的顺序性保证
1 2 3 4
| int new_key = key; if(g[nx][ny] == key + 1) { new_key = key + 1; }
|
只有遇到数字
key+1 时,才更新钥匙状态,确保顺序正确。
# 5. 搜索实现细节
# 数据结构
1 2 3 4 5 6 7
| struct State { int x, y; int key; };
int dist[maxn][maxn][10];
|
# BFS 流程
1 2 3 4 5 6 7 8 9 10
| 1. 初始化 dist[起点][0] = 0 2. 将状态(起点, 0)加入队列 3. while 队列非空: 4. 取出队首状态 5. 如果是终点且key==m: 返回结果 6. 向四个方向扩展: 7. 越界/撞墙: 跳过 8. 计算新钥匙状态 9. 计算时间花费(考虑蛇房间) 10. 如果能更新dist: 入队
|
# 6. 复杂度分析
- 状态数: O (n² × m) = 100×100×10 ≈ 10⁵
- 每个状态扩展: 4 个方向
- 总操作: ≈ 4×10⁵,完全可行
- 空间复杂度: O (n² × m) ≈ 10⁵ × 4 字节 ≈ 400KB
# 📝 完整代码解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
| #include <bits/stdc++.h> #define int long long using namespace std;
const int inf = 2e18 + 9; int dir[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
struct State { int x, y; int key; };
const int maxn = 110; int g[maxn][maxn];
int dist[maxn][maxn][10];
void solve() { int n, m; cin >> n >> m; int start_x, start_y; int target_x, target_y; for(int i = 0; i < n; i++) { for(int j = 0; j < n; j++) { char c; cin >> c; if(c == 'K') { start_x = i; start_y = j; g[i][j] = -1; } else if(c == '#') g[i][j] = -3; else if(c == '.') g[i][j] = -1; else if(c == 'S') g[i][j] = -2; else if(c == 'T') { target_x = i; target_y = j; g[i][j] = -1; } else g[i][j] = c - '0'; for(int k = 0; k <= m; k++) { dist[i][j][k] = inf; } } } queue<State> q; q.push({start_x, start_y, 0}); dist[start_x][start_y][0] = 0; while(!q.empty()) { auto [x, y, key] = q.front(); q.pop(); if(x == target_x && y == target_y && key == m) { cout << dist[x][y][key] << endl; return; } for(int d = 0; d < 4; d++) { int nx = x + dir[d][0]; int ny = y + dir[d][1]; if(nx < 0 || nx >= n || ny < 0 || ny >= n) continue; if(g[nx][ny] == -3) continue; int new_key = key; if(g[nx][ny] == key + 1) { new_key = key + 1; } int time_cost; if(g[nx][ny] == -2) { time_cost = 2; g[nx][ny] = -1; } else { time_cost = 1; } int new_time = dist[x][y][key] + time_cost; if(new_time < dist[nx][ny][new_key]) { dist[nx][ny][new_key] = new_time; q.push({nx, ny, new_key}); } } } cout << "impossible" << endl; }
signed main() { ios::sync_with_stdio(false); cin.tie(0); int t = 1; while(t--) { solve(); } return 0; }
|
# 🎓 涉及知识点
# 1. 广度优先搜索 (BFS)
- 用于无权图的最短路径
- 本题中结合状态压缩形成 状态 BFS
# 2. 状态压缩
- 将钥匙收集进度作为状态维度
- 避免重复访问相同状态
- 状态表示:
(位置, 钥匙进度)
# 3. 动态规划思想
dist[x][y][k] 数组记录最短时间- 状态转移方程:
1 2 3 4
| dist[nx][ny][new_key] = min( dist[nx][ny][new_key], dist[x][y][key] + time_cost )
|
# 4. 网格图遍历
- 四方向移动:上下左右
- 边界判断:0 ≤ x,y < n
# ⚠️ 注意事项与边界情况
# 1. 输入保证
n ≤ 100 , m ≤ 9K 和 T 各出现一次S 最多 5 个- 数字 1-m 至少出现一次
# 2. 常见错误
- 忘记初始化 dist 数组:应初始化为无穷大
- 钥匙顺序判断错误:必须是
key+1 才收集 - 蛇房间标记时机:只有在首次访问时才标记
- 终点判断条件:必须同时满足位置和钥匙条件
# 📊 示例分析
# 示例 1
1 2 3 4 5 6 7
| 输入: 3 2 K12 ..# S.T 输出: 6
|
路径解释:
- K→1 (1 分钟)
- 1→2 (1 分钟,收集钥匙 2)
- 2→S (1 分钟)
- S 停留 (额外 1 分钟,打倒蛇)
- S→. (1 分钟)
- .→T (1 分钟)
总计:6 分钟
# 示例 2
1 2 3 4 5 6
| 输入: 2 1 K# T1 输出: impossible
|
钥匙 1 不可达,无法救出师傅。
# 💡 优化思考
# 1. 进一步状态压缩
如果题目要求更严格(如蛇房间较多),可用二进制状态记录:
1 2 3 4 5
| struct State { int x, y; int key_state; int snake_state; };
|
# 2. 双向 BFS
可从起点和终点同时搜索,在中间状态相遇,能显著减少搜索空间。
# 3. A * 启发式搜索
使用曼哈顿距离作为启发函数,可加速搜索过程。
# 🏆 总结
本题是一道 状态 BFS 的经典题目,关键在于:
- 将钥匙收集进度作为状态维度
- 巧妙处理蛇房间(首次访问特殊处理)
- 使用三维数组记录最短时间
- 严格保证钥匙收集顺序
通过 BFS + 状态标记 的方法,可以在合理时间内求解,是图论与状态压缩结合的典型应用。