2020安徽省大学生程序设计大赛题解——E 收集圣物
E 收集圣物
在一个策略游戏中,僧侣单位可以收集圣物,放入修道院中,以生产黄金。因此,圣物对赢得游戏的胜利很重要。
某个僧侣正处于一个长为n 宽为m 的矩形迷宫中,迷宫的四周都是墙壁,不可移动。迷宫中圣物可能有若干个,为了降低游戏难度,僧侣只需要发现任意一个圣物,就能完成任务。
迷宫有若干扇门。同时,在迷宫的一些地方会有钥匙。每一把钥匙都只能使用一次,每一次可以开一扇门,即:一旦你通过一扇门后,门将自动关闭,如果想要再次打开这扇门,你需要再使用一把钥匙去打开它,通过之后门还将自动关闭。
输入这个迷宫,输出最快多少秒能够发现一个圣物。僧侣移速缓慢,每秒只能够向上下左右四个方向各移动一个单位。不过开门以及拾取钥匙不耗费时间。
如果僧侣无法收集任何圣物,输出“Bug Maze ! ! ! ”。
题目
标签
队列,图的BFS,状态压缩,状态转移
分析
类似于迷宫求最短路径问题,与传统问题不同的是,本题含有多个终点,并且前往终点的路上增加了门钥匙这一条件。另外,本题可能无解,因此需要对传统的判断条件加以改变。
考虑传统问题的解决办法,最直接思路是图的 B F S BFS BFS算法。我们设置设置一个 d [ i ] [ j ] d[i][j] d[i][j]的二维数组来存储从起点到坐标 ( i , j ) (i,j) (i,j)的最短距离,通过不断的遍历进行寻迹,并把每个状态与最优状态进行比较,以确定最终的最短路径。
因为本题新增了元素门钥匙,原有的算法需要做出相应的改变。最直观的变化体现在 d [ i ] [ j ] d[i][j] d[i][j]上,因为不符合门钥匙要求的最短路径不具有讨论的意义。
为了解决这个问题,我们需要再加入两个状态,用四维数组 ∗ ∗ ∗ ∗ d ****d ∗∗∗∗d来对门钥匙的情况进行记录,每个坐标现有四个状态 ( x , y , c n t , s t a t e ) (x,y,cnt,state) (x,y,cnt,state), d [ x ] [ y ] [ c n t ] [ s t a t e ] d[x][y][cnt][state] d[x][y][cnt][state]表示在去 x x x, y y y这个位置,当前有 c n t cnt cnt把钥匙,当前地图中钥匙的状态为 s t a t e state state时,最小路径是多少。通过增加的这两个状态来解决是否有钥匙开门以及不能重复拾取钥匙的问题。
对于数据集钥匙数量小于等于 10 10 10的要求,我们将钥匙的状态表示成二进制 0000000000 0000000000 0000000000,即十个 0 0 0,如果我们转移到下一个位置的时候,拿到了之前初始化编号的第一把钥匙,对应位置 s t a t e state state就从 0000000000 0000000000 0000000000变成 0000000001 0000000001 0000000001,即 0 0 0表示该编号位置的钥匙没被拿过, 1 1 1表示该编号位置的钥匙被拿过了。
数组 c h a r g [ 26 ] [ 26 ] char \ g[26][26] char g[26][26]存储用户输入的迷宫符号,首先逐行读入迷宫的各状态,并分别存储起点坐标和钥匙编号,将每个钥匙的坐标即编号存入 m a p < P I I , i n t > m p map<PII, int> mp map<PII,int>mp,然后从起点开始 B F S BFS BFS算法。
值得注意的是,这里的 ∗ ∗ g **g ∗∗g理解为由字符串组成的数组。为了匹配本题的需求,我们把字符串的起始位置定义为 1 1 1,因此数据的读入部分如下:
for (int i = 1; i <= n; i++) cin >> g[i] + 1;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
if (g[i][j] == 'S')
sx = i, sy = j; //标记起点
else if (g[i][j] == 'K') {
mp[make_pair(i, j)] = cnt;
cnt++;
}
与传统迷宫问题最大的不同在于后面的部分,即状态压缩与状态转移。我们已经完成了状态压缩,即用 ∗ ∗ ∗ ∗ d ****d ∗∗∗∗d表示当前路径长度和钥匙的状态,现在考虑状态转移。
我们在遇到 K K K即钥匙时,首先判断这个钥匙的编号 i d id id,然后再看这个钥匙是否被拿过,即判断当前 s t a t e state state的第 i d id id位是否是 1 1 1,如果是1,则将当前位置当空地处理。如果是 0 0 0,则说明该钥匙没被吃过,当前钥匙数 + 1 +1 +1,并将当前 s t a t e state state的第 i d id id位标记为一。
然后是
B
F
S
BFS
BFS算法:
找到从起点到终点的最短路径其实就是一个建立队列的过程:
- 从起点开始,将其加入队列,设置距离为 0 0 0
- 从队列首端取出位置,将从这个位置能够到达的位置加入队列,并且让这些位置的距离为上一个位置的距离加上 1 1 1
- 一轮探索至无法继续向队列中添加元素,即当前所在点的四周情况全部考虑完成后,循环第二步,直到将圣物添加到队列中。说明此时已经找到了最短路径
参考答案(C++)
#include<bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII; //存坐标
const int N = 30;
const int dx[] = { 1,-1,0,0 }, dy[] = { 0,0,1,-1 };//人行进的方向
int n, m;
char g[26][26]; //迷宫
int d[26][26][11][1030]; //到当前状态的最小距离
int cnt = 0; //当前的钥匙个数
map<PII, int> mp; //记录钥匙的编号
bool vaild(int x, int y) { return x > 0 && x <= n && y > 0 && y <= m; }//判断是否在迷宫中
struct Node { //记录当前的状态
int x, y, cnt, state; //x,y表示当前的位置 cnt表示当前有多少把钥匙 state表示当前哪些位置的钥匙被吃过了
Node() {}
Node(int x, int y, int cnt, int state) :x(x), y(y), cnt(cnt), state(state) {}
};
int bfs(int sx, int sy) { //从人当前开始广度优先搜索遍历
memset(d, -1, sizeof(d));
queue<Node> Q;
Q.push({ sx,sy,0,0 }); //放入第一个人的当前状态入队
d[sx][sy][0][0] = 0;
while (!Q.empty()) {
auto t = Q.front();
Q.pop();
int x = t.x, y = t.y, cnt = t.cnt, state = t.state;
int dd = d[x][y][cnt][state];
for (int i = 0; i < 4; i++) {
int nx = x + dx[i], ny = y + dy[i];
if (!vaild(nx, ny)) continue;
if (g[nx][ny] == '#') continue;
if (g[nx][ny] == 'E') return dd + 1;
if (g[nx][ny] == '.') {
d[nx][ny][cnt][state] = dd + 1;
Q.push({ nx,ny,cnt,state });
}
if (g[nx][ny] == 'K') {
PII now = { nx,ny };
int id = mp[now];
if (state & (1 << id)) {//表示state的第id位是1
d[nx][ny][cnt][state] = dd + 1;
Q.push({ nx,ny,cnt,state });
}
else {
d[nx][ny][cnt + 1][state | (1 << id)] = dd + 1;
Q.push({ nx,ny,cnt + 1,state | (1 << id) });
}
}
if (g[nx][ny] == 'D' && cnt > 0) {
d[nx][ny][cnt - 1][state] = dd + 1;
Q.push({ nx,ny,cnt - 1,state });
}
}
}
return -1;
}
void solve() {
cin >> n >> m; //输入迷宫的行列
for (int i = 1; i <= n; i++) cin >> g[i] + 1; //输入迷宫各状态,由于数据是一行一行的给的,故
int sx = 0, sy = 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
if (g[i][j] == 'S') sx = i, sy = j; //标记人
else if (g[i][j] == 'K') {
mp[make_pair(i, j)] = cnt;
cnt++;
}
int ans = bfs(sx, sy);
if (ans == -1) cout << "Bug Maze!!!";
else cout << ans << '\n';
}
int main() {
solve();
return 0;
}