题目地址:
https://www.acwing.com/problem/content/description/176/
推箱子游戏相信大家都不陌生,在本题中,你将控制一个人把
1
1
1个箱子到目的地。给定一张
N
N
N行
M
M
M列的地图,用字符.
表示空地,字符#
表示墙,字符S
表示人的起始位置,字符B
表示箱子的起始位置,字符T
表示箱子的目标位置。求一种移动方案,使箱子移动的次数最少,在此基础上再让人移动的总步数最少。方案中使用大写的EWSN
(东西南北)表示箱子的移动,使用小写的ewsn
(东西南北)表示人的移动。
输入格式:
输入包含多个测试用例。对于每个测试用例,第一行包括两个整数
N
,
M
N, M
N,M。接下来
N
N
N行,每行包括
M
M
M个字符,用以描绘整个
N
N
N行
M
M
M列的地图。当样例为
N
=
0
,
M
=
0
N=0,M=0
N=0,M=0时,表示输入终止,且该样例无需处理。
输出格式:
对于每个测试用例,第一行输出Maze #
+
+
+测试用例的序号。第二行输入一个字符串,表示推箱子的总体移动过程,若无解,则输出Impossible.
。每个测试用例输出结束后输出一个空行。若有多条路线满足题目要求,则按照N, S, W, E
的顺序优先选择箱子的移动方向(即先上下推,再左右推)。在此前提下,再按照n, s, w, e
的顺序优先选择人的移动方向(即先上下动,再左右动)。
数据范围:
1
≤
N
,
M
≤
20
1≤N,M≤20
1≤N,M≤20
思路是BFS。考虑如何定义每个状态,除了箱子要计入状态之外,人的位置也要计入状态,因为人的位置会影响下一步箱子能推到哪里。由于要按箱子的最短路优先的次序来搜索答案,所以我们按箱子移动步数一层一层向外扩展,每次扩展的时候,看人是否能走到相应的位置使得箱子从当前状态走到下一个状态。由于路径也要按字典序优先顺序搜索,所以我们搜索的时候,每一步按照上下左右的顺序依次搜索。具体看代码:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
#include <queue>
using namespace std;
using PII = pair<int, int>;
const int N = 22;
// Node为箱子的状态,也是搜索空间的点,dir表示的是箱子到达(x, y)位置的上一步的朝向下标
struct Node {
int x, y, dir;
};
int n, m;
char g[N][N];
// 存的是箱子当前状态的上一个状态
Node pre[N][N][4];
// 存的是人的路径中每个位置的上一步的朝向下标
int pre_man[N][N];
// path存的是为了使得箱子到达(j, k, i)这个状态,人所走过的路径的朝向向量,路径是倒着存储的
vector<int> path[N][N][4];
bool vis[N][N][4], vis_man[N][N];
// dist存的是箱子到达(j, k, i)这个状态时,箱子走过的路径长度和人走过的路径长度的pair
PII dist[N][N][4];
// 方向按照上下左右的顺序排列
int dx[4] = {-1, 1, 0, 0}, dy[4] = {0, 0, -1, 1};
char ops[] = "NSWE";
// 判断(x, y)这个位置是否是合法的位置
bool check(int x, int y) {
return 0 <= x && x < n && 0 <= y && y < m && g[x][y] != '#';
}
// 返回当人在start,箱子在box,并且人要走到end的最少步数,步数相等时取字典序最小的路径,并将路径存入seq中
int bfs_man(PII start, PII end, PII box, vector<int> &seq) {
memset(vis_man, 0, sizeof vis_man);
memset(pre_man, -1, sizeof pre_man);
// 如果起点等于终点则不用走了,步数为0
if (start == end) return 0;
queue<PII> q;
q.push(start);
// 标记起点为走过
vis_man[start.first][start.second] = true;
// 也要标记箱子的位置为走过,因为箱子是不能走到的
vis_man[box.first][box.second] = true;
while (q.size()) {
auto t = q.front(); q.pop();
for (int i = 0; i < 4; i++) {
// 当前位置为(x, y)
int x = t.first, y = t.second;
// 尝试走到(a, b),这里搜人下一步的时候要按照上下左右的顺序来搜
int a = x + dx[i], b = y + dy[i];
if (check(a, b) && !vis_man[a][b]) {
vis_man[a][b] = true;
// 存一下走到(a, b)时最后一步走的朝向
pre_man[a][b] = i;
// 如果走到了end,就求一下路径,并返回人的步数
if (a == end.first && b == end.second) {
// 倒着存路径
x = a, y = b;
while (pre_man[x][y] != -1) {
int dir = pre_man[x][y];
seq.push_back(dir);
x -= dx[dir], y -= dy[dir];
}
return seq.size();
}
q.push({a, b});
}
}
}
return -1;
}
bool bfs_box(PII man, PII box, Node &end) {
memset(vis, 0, sizeof vis);
queue<Node> q;
// 以箱子从初始状态移动一步所得da
for (int i = 0; i < 4; i++) {
int x = box.first, y = box.second;
// (a, b)是人要走到的位置,走过去之后,下一步就是推箱子
int a = x - dx[i], b = y - dy[i];
// (j, k)是箱子要被推到的位置
int j = x + dx[i], k = y + dy[i];
vector<int> seq;
// 如果人要走到的位置和箱子要被推到的位置都合法,并且人确实能走过去推的话,说明这个状态可以搜到
if (check(a, b) && check(j, k) && ~bfs_man(man, {a, b}, box, seq)) {
dist[j][k][i] = {1, seq.size()};
path[j][k][i] = seq;
pre[j][k][i] = {x, y, -1};
// 如果箱子走一步就能到终点,则直接返回
if (g[j][k] == 'T') {
end = {j, k, i};
return true;
}
vis[j][k][i] = true;
q.push({j, k, i});
}
}
bool success = false;
// 存当前搜到的”最优路径“中箱子移动步数和人移动步数
PII best_dis = {1e9, 1e9};
while (q.size()) {
auto t = q.front(); q.pop();
for (int i = 0; i < 4; i++) {
// 存人走到的位置,下一步要推箱子
int a = t.x - dx[i], b = t.y - dy[i];
// 存箱子下一步会被推到的位置
int j = t.x + dx[i], k = t.y + dy[i];
// 如果这两个位置都合法,则继续看人能不能走到(a, b)这个位置
if (check(a, b) && check(j, k)) {
vector<int> seq;
// dis存人从当前位置能否走到(a, b)这个位置,如果能,dis存最少步数,否则存-1
int dis = bfs_man({t.x - dx[t.dir], t.y - dy[t.dir]}, {a, b}, {t.x, t.y}, seq);
if (~dis) {
// 存新状态中箱子的总移动步数和人的总移动步数
PII td = {dist[t.x][t.y][t.dir].first + 1, dist[t.x][t.y][t.dir].second + dis};
// 如果新状态以前未被访问,或者虽然被访问,但是当前扩展出的方案更优(即人移动的步数更少),则做更新
if (!vis[j][k][i] || dist[j][k][i] > td) {
path[j][k][i] = seq;
pre[j][k][i] = t;
dist[j][k][i] = td;
// 如果新状态恰好就是终点,则标记找到,并且看一下是否是更优方案
if (g[j][k] == 'T') {
success = true;
if (best_dis > dist[j][k][i]) {
end = {j, k, i};
best_dis = dist[j][k][i];
}
vis[j][k][i] = true;
}
if (!vis[j][k][i] && !success) {
vis[j][k][i] = true;
q.push({j, k, i});
}
}
}
}
}
}
return success;
}
int main() {
int T = 1;
while (scanf("%d%d", &n, &m), n || m) {
printf("Maze #%d\n", T++);
for (int i = 0; i < n; i++) cin >> g[i];
PII man, box;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
if (g[i][j] == 'S') man = {i, j};
else if (g[i][j] == 'B') box = {i, j};
Node end;
if (!bfs_box(man, box, end)) puts("Impossible.");
else {
string res;
while (!(end.x == box.first && end.y == box.second && end.dir == -1)) {
res += ops[end.dir];
for (auto dir: path[end.x][end.y][end.dir]) res += ops[dir] + 32;
end = pre[end.x][end.y][end.dir];
}
reverse(res.begin(), res.end());
cout << res << endl;
}
puts("");
}
return 0;
}
时间复杂度 O ( ( N M ) 2 ) O((NM)^2) O((NM)2),空间 O ( N M ) O(NM) O(NM)。