题目
在《无主星渊》的太空战场中,玩家操控飞船从起点 S 出发,在 n×m 的网格中以最短时间采集一定能量。飞船每次仅能向上、下、左、右四个方向移动一个网格,能量必须以固定顺序被采集,飞船触碰到能量即视为采集能量。
一共有 5 个能量,依次编号为 1~5,玩家必须先采集编号小的能量,即先采集 1,再采集 2,最终采集 5。
网格包含以下元素:
#:不可穿越的障碍物.:可自由航行的太空区域1~5:表示 5 个能量的编号S:飞船起点
请你帮玩家算一算,玩家最少移动几次可以集齐这 5 个能量。
输入描述
第一行:
n m(1 ≤ n, m ≤ 200)
表示网格的行数和列数。
接下来 n 行,每行一个长度为 m 的字符串,表示网格地图。
输出描述
输出一个整数,表示玩家最少所需的移动次数。
数据保证一定有解。
测试用例
样例 1
输入:
4 4
1.2.
.##3
.##4
S..5
输出:
9
样例 2
输入:
5 5
..4##
3....
##S..
#5...
##1.2
输出:
19
一开始容易想到的思路:递归 / DFS 搜路
这种题第一眼看上去很像迷宫题,很容易产生两个直觉:
- 从
S开始递归搜索; - 每次尝试向四个方向走,看看能不能把
1~5全部吃掉。
这个思路的问题在于:题目问的是“最少移动次数” ,而不是“能不能到达”。
这两个目标对应的常见算法并不一样:
- 如果题目问的是 有没有路径,那 DFS / BFS 都可以;
- 如果题目问的是 最少步数 / 最短路,那优先应该想到 BFS。
也就是说,这题从一开始就不应该往递归 DFS 上靠,而应该优先往 网格最短路 上靠。
我在这题里踩过的坑
这题我不是一下子就写对的,中间踩了不少坑。这里把它们完整整理出来。
坑 1:把这题当成“普通迷宫最短路”
一开始如果只看到:
- 上下左右走
#不能走- 从
S走到某些点
很容易直接写一个普通 BFS,把所有非 # 的格子都当成可走区域。
但这题多了一个关键条件:
必须按顺序采集
1 -> 2 -> 3 -> 4 -> 5,且飞船触碰到能量就算采集。
这意味着:
- 在去
1的路上,不能提前踩到2、3、4、5 - 在去
2的路上,不能提前踩到3、4、5 - 在去
3的路上,不能提前踩到4、5
所以这题不是普通 BFS,而是:
带编号上界限制的分段 BFS
坑 2:没意识到这题可以“拆段”
一开始很容易把整题想成:
从
S一路搜到最终状态(已经吃完1~5)
这当然也能做,但状态会复杂很多。
实际上,这题因为采集顺序是固定的,所以天然可以拆成 5 段:
S -> 1
1 -> 2
2 -> 3
3 -> 4
4 -> 5
总答案就是这 5 段最短路之和。
也就是说:
整题不是一个“大搜索”,而是 5 个“小搜索”的和。
这是这道题最关键的建模转折。
坑 3:手搓四个方向判断,容易写乱
我一开始写 BFS 时,习惯手写四个方向:
- 上
- 下
- 左
- 右
这样写的问题是:
- 很容易把
n / m写反 - 很容易某个方向条件不统一
- 很容易漏掉越界判断
例如我就写错过:
x和m比y和n比
这其实是非常典型的网格题 bug。
后面我才意识到,网格四方向应该直接模板化:
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};
然后统一循环四个方向。
坑 4:想手工维护层数 ans++
我一开始还尝试过:
- 用双队列分层
- 每扩展一层时手工
ans++
这种写法不是不能做,但非常容易出问题:
- 容易把“扩展一个节点”错当成“走了一步”
- 容易把
ans++放错位置 - 容易在 BFS 中同时维护队列和层数,代码变复杂
后来我才意识到:
最短步数 BFS 最稳的写法,是直接用
dist数组记录到每个点的最短距离。
比如:
dist[nx][ny] = dist[x][y] + 1;
这样第一次到终点时,dist 就已经是答案,根本不需要手工数层数。
坑 5:CanGo() 没抽出来,导致条件判断又长又容易错
我一开始把“一个格子能不能走”的判断,直接写在 BFS 的四个方向扩展里。
结果就是:
- 条件很长
- 容易重复
- 容易漏判
尤其在这题里,合法格子不只是“不是 #”这么简单,还要考虑当前目标编号 k:
#不能走.能走S能走- 数字
<= k能走 - 数字
> k不能走
如果不抽成函数,代码会非常乱。
正确做法应该是:
bool CanGo(char c, int k)
把这个逻辑收口到一个地方。
坑 6:C++ 容器语法不熟导致的实现错误
这题里我还暴露出几个 BFS 常见的 C++ 基础问题:
问题 1:queue 不能范围 for
不能写:
for (auto x : q)
queue 不是可迭代容器。
正确写法是:
auto cur = q.front();
q.pop();
问题 2:queue 没有 clear()
如果你想清空队列,不能直接写 q.clear()。
问题 3:pair<int,int> 不能写 i[0]
应该写:
i.first
i.second
或者:
auto [x, y] = cur;
问题 4:main 里不能 return ans
如果你想输出答案,应该:
cout << ans << endl;
不是 return ans;
因为 return 返回的是程序退出码,不是题目输出。
正确思路
一、固定顺序,天然拆成 5 段
因为必须按顺序采集,所以整题可以拆成:
S -> 1
1 -> 2
2 -> 3
3 -> 4
4 -> 5
总答案就是:
dist(S,1) + dist(1,2) + dist(2,3) + dist(3,4) + dist(4,5)
二、每一段都是无权图最短路
网格中每走一步代价都相同,都是 1。
所以每一段都是标准的 无权图最短路问题,用 BFS 最合适。
三、BFS 时加上当前目标编号限制
当前目标是 k 时,一个格子能不能走,规则如下:
#:不能走.:能走S:能走1~5:只有当编号<= k时能走
所以我们可以抽出:
bool CanGo(char c, int k)
四、用 dist 记录最短步数
定义:
dist[i][j]
表示从当前段起点走到 (i,j) 的最短距离。
初始化:
- 全部设成
-1 - 起点设成
0
转移时:
dist[nx][ny] = dist[x][y] + 1;
第一次到达终点时,直接返回 dist[x][y]。
完整正确代码
#include <iostream>
#include <vector>
#include <queue>
#include <string>
using namespace std;
bool CanGo(char c, int k) {
if (c == '#') return false;
if (c == '.' || c == 'S') return true;
if (c >= '1' && c <= '5') return (c - '0') <= k;
return false;
}
int BFS(const vector<string>& mp, pair<int, int> start, pair<int, int> target, int k) {
int n = mp.size();
int m = mp[0].size();
vector<vector<int>> dist(n, vector<int>(m, -1));
queue<pair<int, int>> q;
q.push(start);
dist[start.first][start.second] = 0;
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};
while (!q.empty()) {
auto cur = q.front();
q.pop();
int x = cur.first;
int y = cur.second;
if (x == target.first && y == target.second) {
return dist[x][y];
}
for (int t = 0; t < 4; t++) {
int nx = x + dx[t];
int ny = y + dy[t];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
if (dist[nx][ny] != -1) continue;
if (!CanGo(mp[nx][ny], k)) continue;
dist[nx][ny] = dist[x][y] + 1;
q.push({nx, ny});
}
}
return -1;
}
int main() {
int n, m;
cin >> n >> m;
vector<string> mp(n);
vector<pair<int, int>> pos(6);
for (int i = 0; i < n; i++) {
cin >> mp[i];
for (int j = 0; j < m; j++) {
if (mp[i][j] == 'S') {
pos[0] = {i, j};
} else if (mp[i][j] >= '1' && mp[i][j] <= '5') {
pos[mp[i][j] - '0'] = {i, j};
}
}
}
int ans = 0;
ans += BFS(mp, pos[0], pos[1], 1);
ans += BFS(mp, pos[1], pos[2], 2);
ans += BFS(mp, pos[2], pos[3], 3);
ans += BFS(mp, pos[3], pos[4], 4);
ans += BFS(mp, pos[4], pos[5], 5);
cout << ans << endl;
return 0;
}
复杂度分析
设网格大小为 n × m。
每一段 BFS 最多遍历整个网格一次,所以复杂度为:
O(n*m)
总共 5 段,因此总复杂度为:
O(5*n*m) = O(n*m)
空间复杂度为:
O(n*m)
用于保存 dist 数组和 BFS 队列。
这题最后提炼出的经验
- 看到“最少步数 / 最短移动次数”,优先想 BFS。
- 顺序固定的问题,优先考虑拆段,不要一上来就做大状态搜索。
- 网格 BFS 里,四方向扩展优先用
dx / dy。 - 重复判断优先抽函数,例如
CanGo()。 - BFS 求最短路优先用
dist,不要手工数层数。