题目描述
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,两个格子的状态为k
int 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;
}