每周题解:拯救大兵瑞恩

题目描述

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 Gi1 时,表示 ( 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 Xi1Xi2+Yi1Yi2=1,
0 ≤ G i ≤ P 0≤G_i≤P 0GiP,
1 ≤ Q i ≤ P 1≤Q_i≤P 1QiP,
1 ≤ N , M , P ≤ 10 1≤N,M,P≤10 1N,M,P10,
1 ≤ k ≤ 150 1≤k≤150 1k150

算法思想

状态表示

根据题目描述,从 ( 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=(x1)×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;
}
  • 27
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

少儿编程乔老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值