题目描述
1944 年,特种兵麦克接到国防部的命令,要求立即赶赴太平洋上的一个孤岛,营救被敌军俘虏的大兵瑞恩。
瑞恩被关押在一个迷宫里,迷宫地形复杂,但幸好麦克得到了迷宫的地形图。
迷宫的外形是一个长方形,其南北方向被划分为 N N N 行,东西方向被划分为 M M M 列, 于是整个迷宫被划分为 N × M N×M N×M 个单元。
每一个单元的位置可用一个有序数对 (单元的行号, 单元的列号) 来表示。
南北或东西方向相邻的 2 2 2 个单元之间可能互通,也可能有一扇锁着的门,或者是一堵不可逾越的墙。
注意: 门可以从两个方向穿过,即可以看成一条无向边。
迷宫中有一些单元存放着钥匙,同一个单元可能存放 多把钥匙,并且所有的门被分成 P P P 类,打开同一类的门的钥匙相同,不同类门的钥匙不同。
大兵瑞恩被关押在迷宫的东南角,即 ( N , M ) (N,M) (N,M) 单元里,并已经昏迷。
迷宫只有一个入口,在西北角。也就是说,麦克可以直接进入 ( 1 , 1 ) (1,1) (1,1) 单元。
另外,麦克从一个单元移动到另一个相邻单元的时间为 1 1 1,拿取所在单元的钥匙的时间以及用钥匙开门的时间可忽略不计。
试设计一个算法,帮助麦克以最快的方式到达瑞恩所在单元,营救大兵瑞恩。
输入格式
第一行有三个整数,分别表示 N , M , P N,M,P N,M,P 的值。
第二行是一个整数 k k k,表示迷宫中门和墙的总数。
接下来 k k k 行,每行包含五个整数, X i 1 , Y i 1 , X i 2 , Y i 2 , G i X_{i1},Y_{i1},X_{i2},Y_{i2},G_i Xi1,Yi1,Xi2,Yi2,Gi:当 G i ≥ 1 G_i≥1 Gi≥1 时,表示 ( X i 1 , Y i 1 ) (X_{i1},Y_{i1}) (Xi1,Yi1) 单元与 ( X i 2 , Y i 2 ) (X_{i2},Y_{i2}) (Xi2,Yi2) 单元之间有一扇第 G i G_i Gi 类的门,当 G i = 0 G_i=0 Gi=0 时,表示 ( X i 1 , Y i 1 ) (X_{i1},Y_{i1}) (Xi1,Yi1) 单元与 ( X i 2 , Y i 2 ) (X_{i2},Y_{i2}) (Xi2,Yi2) 单元之间有一面不可逾越的墙。
接下来一行,包含一个整数 S S S,表示迷宫中存放的钥匙的总数。
接下来 S S S 行,每行包含三个整数 X i 1 , Y i 1 , Q i X_{i1},Y_{i1},Q_i Xi1,Yi1,Qi,表示 X i 1 , Y i 1 X_{i1},Y_{i1} Xi1,Yi1 单元里存在一个能开启第 Q i Q_i Qi 类门的钥匙。
输出格式
输出麦克营救到大兵瑞恩的最短时间。
如果问题无解,则输出 -1
。
样例 #1
样例输入 #1
4 4 9
9
1 2 1 3 2
1 2 2 2 0
2 1 2 2 0
2 1 3 1 0
2 3 3 3 0
2 4 3 4 1
3 2 3 3 0
3 3 4 3 0
4 3 4 4 0
2
2 1 2
4 2 1
样例输出 #1
14
提示
【样例解释】
测试样例的迷宫如下图所示
【数据范围】
∣ X i 1 − X i 2 ∣ + ∣ Y i 1 − Y i 2 ∣ = 1 |X_{i1}−X_{i2}|+|Y_{i1}−Y_{i2}|=1 ∣Xi1−Xi2∣+∣Yi1−Yi2∣=1,
0 ≤ G i ≤ P 0≤G_i≤P 0≤Gi≤P,
1 ≤ Q i ≤ P 1≤Q_i≤P 1≤Qi≤P,
1 ≤ N , M , P ≤ 10 1≤N,M,P≤10 1≤N,M,P≤10,
1 ≤ k ≤ 150 1≤k≤150 1≤k≤150
算法思想
状态表示
根据题目描述,从 ( 1 , 1 ) (1,1) (1,1)出发,每次移动一个单元,时间为 1 1 1,求的是走到 ( n , m ) (n,m) (n,m)点的最短时间。如果不考虑钥匙和门的情况下,可以直接用BFS求解从 ( 1 , 1 ) (1, 1) (1,1)走到任意一点 ( x , y ) (x,y) (x,y)的最小步数 d i s ( x , y ) dis(x,y) dis(x,y)。
加上钥匙和门之后, d i s ( x , y ) dis(x,y) dis(x,y)显然无法表达走到点 ( x , y ) (x,y) (x,y)时拥有钥匙的状态。那么可以进行拆点,利用状态压缩的思想,引入一个 s t a t e state state。 d i s ( x , y , s t a t e ) dis(x,y,state) dis(x,y,state)表示从 ( 1 , 1 ) (1, 1) (1,1)走到任意 ( x , y ) (x,y) (x,y)、并且当前拥有的钥匙状态为 s t a t e state state时的最小步数。例如 s t a t e state state二进制为 ( 0110 ) 2 (0110)_2 (0110)2时,表示持有第 1 , 2 1,2 1,2类钥匙,因为钥匙编号从 1 1 1开始,要判断是否持有第 i i i类钥匙时,只需要判断state >> i & 1
是否为 1 1 1即可。
为了方便判断两个格子的状态(互通、墙、还是门),这里可以将二维坐标 ( x , y ) (x,y) (x,y)转换成一维的编号 z z z,如下图所示:
这样 d i s ( z , s t a t e ) dis(z,state) dis(z,state)表示走到编号为 z z z的格子、并且当前拥有的钥匙状态为 s t a t e state state时的最小步数,其中 z = ( x − 1 ) × m + y z=(x-1)\times m + y z=(x−1)×m+y。
状态计算
在计算 d i s ( z , s t a t e ) dis(z,state) dis(z,state)时,由于只能向东南西北 4 4 4个方向进行移动,那么可以利用偏移数组进行状态转移,在转移过程中可以分为下面几种情况:
- 两个相邻格子间有墙,不能转移;
- 两个相邻格子间有门,没有该类门钥匙,不能转移;
- 两个相邻格子间有门,拥有该类门钥匙,能够转移,最小步数 + 1 +1 +1
- 两个相邻格子间没有障碍,能够转移,最小步数 + 1 +1 +1
在转移过程中还要考虑拥有的钥匙状态:
- 如果转移到的格子上没有钥匙,则钥匙状态不变
- 如果转移到的格子上拥有钥匙的状态为 s s s,则钥匙状态更新为
s=s|state
当走到终点时,也就是编号为 n × m n\times m n×m的格子,此时不关心到达终点时拥有钥匙的状态,只要到达了终点就能成功营救大兵瑞恩。
可以看出在整个状态计算的过程中,只要状态转移了,步长都是+ 1 1 1的,使用普通的BFS就可以解决了。
时间复杂度
BFS算法每个状态只会入队出队 1 1 1次,因此时间复杂度跟状态数量有关,为 O ( n × m × 2 p ) O(n\times m\times 2^p) O(n×m×2p)
代码实现
#include <bits/stdc++.h>
using namespace std;
const int N = 105, M = 12;
typedef pair<int, int> PII;
int n, m, p;
//g表示格子之间的状态:-1表示无障碍、0表示墙、k表示k类门
//key表示格子中的钥匙类型
int g[N][N], key[N];
int dis[N][1 << M]; //状态
bool st[N][1 << M]; //标记数组
int dx[] = {-1, 0, 1, 0}, dy[] = {0, 1, 0, -1};
int get(int x, int y) //获取格子的编号
{return (x - 1) * m + y;
}
int bfs()
{memset(dis, 0x3f, sizeof dis);int z = get(1, 1), state = key[z]; //起点编号和状态queue<PII> q; //队列存点的编号和钥匙状态dis[z][state] = 0, st[z][state] = true;q.push({z, state});while(q.size()){PII p = q.front(); q.pop();int z1 = p.first, state = p.second;if(z1 == n * m) return dis[z1][state]; //走到终点int x = (z1 - 1) / m + 1, y = (z1 - 1) % m + 1;for(int i = 0; i < 4; i ++){int a = x + dx[i], b = y + dy[i];if(a < 1 || a > n || b < 1 || b > m) continue;//转移到编号为z2的格子,z2号格子的钥匙状态为s,两个格子的状态为kint z2 = get(a, b), s = key[z2], k = g[z1][z2]; if(k == 0) continue; //有墙if(k >= 1 && !(state >> k & 1)) continue; //有门,没有钥匙s |= state;if(!st[z2][s]){dis[z2][s] = dis[z1][state] + 1, st[z2][s] = true;q.push({z2, s});}}}return -1;
}
int main()
{cin >> n >> m >> p;int K, S;cin >> K;memset(g, -1, sizeof g); //初始化两个格子之间的状态,-1表示无障碍while(K --) //输入门和墙{int x1, y1, x2, y2, k;cin >> x1 >> y1 >> x2 >> y2 >> k;int z1 = get(x1, y1), z2 = get(x2, y2);g[z1][z2] = g[z2][z1] = k; //k为0表示墙、否则k表示k类门}cin >> S;while(S --){int x, y, k;cin >> x >> y >> k;int z = get(x, y);key[z] |= 1 << k; //一个格子可能存在多把钥匙}cout << bfs() << endl;return 0;
}