每周题解:拯救大兵瑞恩

题目描述

1944 年,特种兵麦克接到国防部的命令,要求立即赶赴太平洋上的一个孤岛,营救被敌军俘虏的大兵瑞恩。

瑞恩被关押在一个迷宫里,迷宫地形复杂,但幸好麦克得到了迷宫的地形图。

迷宫的外形是一个长方形,其南北方向被划分为 NNN 行,东西方向被划分为 MMM 列, 于是整个迷宫被划分为 N×MN×MN×M 个单元。

每一个单元的位置可用一个有序数对 (单元的行号, 单元的列号) 来表示。

南北或东西方向相邻的 222 个单元之间可能互通,也可能有一扇锁着的门,或者是一堵不可逾越的墙。

注意: 门可以从两个方向穿过,即可以看成一条无向边。

迷宫中有一些单元存放着钥匙,同一个单元可能存放 多把钥匙,并且所有的门被分成 PPP 类,打开同一类的门的钥匙相同,不同类门的钥匙不同。

大兵瑞恩被关押在迷宫的东南角,即 (N,M)(N,M)(N,M) 单元里,并已经昏迷。

迷宫只有一个入口,在西北角。也就是说,麦克可以直接进入 (1,1)(1,1)(1,1) 单元。

另外,麦克从一个单元移动到另一个相邻单元的时间为 111,拿取所在单元的钥匙的时间以及用钥匙开门的时间可忽略不计。

试设计一个算法,帮助麦克以最快的方式到达瑞恩所在单元,营救大兵瑞恩。

输入格式

第一行有三个整数,分别表示 N,M,PN,M,PN,M,P 的值。

第二行是一个整数 kkk,表示迷宫中门和墙的总数。

接下来 kkk 行,每行包含五个整数,Xi1,Yi1,Xi2,Yi2,GiX_{i1},Y_{i1},X_{i2},Y_{i2},G_iXi1,Yi1,Xi2,Yi2,Gi:当 Gi≥1G_i≥1Gi1 时,表示(Xi1,Yi1)(X_{i1},Y_{i1})(Xi1,Yi1) 单元与 (Xi2,Yi2)(X_{i2},Y_{i2})(Xi2,Yi2) 单元之间有一扇第 GiG_iGi 类的门,当 Gi=0G_i=0Gi=0 时,表示 (Xi1,Yi1)(X_{i1},Y_{i1})(Xi1,Yi1) 单元与 (Xi2,Yi2)(X_{i2},Y_{i2})(Xi2,Yi2) 单元之间有一面不可逾越的墙。

接下来一行,包含一个整数 SSS,表示迷宫中存放的钥匙的总数。

接下来 SSS 行,每行包含三个整数 Xi1,Yi1,QiX_{i1},Y_{i1},Q_iXi1,Yi1,Qi,表示 Xi1,Yi1X_{i1},Y_{i1}Xi1,Yi1 单元里存在一个能开启第 QiQ_iQi 类门的钥匙。

输出格式

输出麦克营救到大兵瑞恩的最短时间。

如果问题无解,则输出 -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

提示

【样例解释】
测试样例的迷宫如下图所示
在这里插入图片描述
【数据范围】

∣Xi1−Xi2∣+∣Yi1−Yi2∣=1|X_{i1}−X_{i2}|+|Y_{i1}−Y_{i2}|=1Xi1Xi2+Yi1Yi2=1,
0≤Gi≤P0≤G_i≤P0GiP,
1≤Qi≤P1≤Q_i≤P1QiP,
1≤N,M,P≤101≤N,M,P≤101N,M,P10,
1≤k≤1501≤k≤1501k150

算法思想

状态表示

根据题目描述,从(1,1)(1,1)(1,1)出发,每次移动一个单元,时间为 111,求的是走到(n,m)(n,m)(n,m)点的最短时间。如果不考虑钥匙和门的情况下,可以直接用BFS求解从(1,1)(1, 1)(1,1)走到任意一点(x,y)(x,y)(x,y)的最小步数dis(x,y)dis(x,y)dis(x,y)

加上钥匙和门之后,dis(x,y)dis(x,y)dis(x,y)显然无法表达走到点(x,y)(x,y)(x,y)时拥有钥匙的状态。那么可以进行拆点,利用状态压缩的思想,引入一个statestatestatedis(x,y,state)dis(x,y,state)dis(x,y,state)表示从(1,1)(1, 1)(1,1)走到任意(x,y)(x,y)(x,y)、并且当前拥有的钥匙状态为statestatestate时的最小步数。例如statestatestate二进制为(0110)2(0110)_2(0110)2时,表示持有第1,21,21,2类钥匙,因为钥匙编号从111开始,要判断是否持有第iii类钥匙时,只需要判断state >> i & 1是否为111即可。

为了方便判断两个格子的状态(互通、墙、还是门),这里可以将二维坐标(x,y)(x,y)(x,y)转换成一维的编号zzz,如下图所示:
在这里插入图片描述
这样dis(z,state)dis(z,state)dis(z,state)表示走到编号为zzz的格子、并且当前拥有的钥匙状态为statestatestate时的最小步数,其中z=(x−1)×m+yz=(x-1)\times m + yz=(x1)×m+y

状态计算

在计算dis(z,state)dis(z,state)dis(z,state)时,由于只能向东南西北444个方向进行移动,那么可以利用偏移数组进行状态转移,在转移过程中可以分为下面几种情况:

  • 两个相邻格子间有墙,不能转移;
  • 两个相邻格子间有门,没有该类门钥匙,不能转移;
  • 两个相邻格子间有门,拥有该类门钥匙,能够转移,最小步数+1+1+1
  • 两个相邻格子间没有障碍,能够转移,最小步数+1+1+1

在转移过程中还要考虑拥有的钥匙状态:

  • 如果转移到的格子上没有钥匙,则钥匙状态不变
  • 如果转移到的格子上拥有钥匙的状态为sss,则钥匙状态更新为s=s|state

当走到终点时,也就是编号为n×mn\times mn×m的格子,此时不关心到达终点时拥有钥匙的状态,只要到达了终点就能成功营救大兵瑞恩。

可以看出在整个状态计算的过程中,只要状态转移了,步长都是+111的,使用普通的BFS就可以解决了。

时间复杂度

BFS算法每个状态只会入队出队111次,因此时间复杂度跟状态数量有关,为O(n×m×2p)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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

少儿编程乔老师

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

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

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

打赏作者

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

抵扣说明:

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

余额充值