超级详解洛谷P4011 孤岛营救问题(bfs超难题)(保证看懂)

题目

说明

1944 年,特种兵麦克接到国防部的命令,要求立即赶赴太平洋上的一个孤岛,营救被敌军俘虏的大兵瑞恩。瑞恩被关押在一个迷宫里,迷宫地形复杂,但幸好麦克得到了迷宫的地形图。迷宫的外形是一个长方形,其南北方向被划分为N 行,东西方向被划分为M列,于是整个迷宫被划分为 N×M 个单元。每一个单元的位置可用一个有序数对(单元的行号,单元的列号)来表示。南北或东西方向相邻的 2 个单元之间可能互通,也可能有一扇锁着的门,或者是一堵不可逾越的墙。迷宫中有一些单啊、元存放着钥匙,并且所有的门被分成 P类,打开同一类的门的钥匙相同,不同类门的钥匙不同。

大兵瑞恩被关押在迷宫的东南角,即(N,M)单元里,并已经昏迷。迷宫只有一个入口,在西北角。也就是说,麦克可以直接进入(1,1)单元。另外,麦克从一个单元移动到另一个相邻单元的时间为1,拿取所在单元的钥匙的时间以及用钥匙开门的时间可忽略不计。

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

输入格式

第 1行有 3个整数,分别表示N,M,P的值。[均小于等于10]

第 2 行是1个整数 K[小于等于150],表示迷宫中门和墙的总数。

第 I+2 行(1<=I<=K) ,有 5 个整数,依次为Xi1,Yi1,Xi2,Yi2,Gi: 当Gi>=1时,表示(Xi1,Yi1)单元与(Xi2,Yi2)单元之间有一扇第Gi类的门,当 Gi=0时,表示(Xi1,Yi1)单元与(Xi2,Yi2)单元之间有一堵不可逾越的墙(其中,|Xi1-Xi2|+|Yi1-Yi2|=1,0<=Gi<=P) 。

第K+3行是一个整数S,表示迷宫中存放的钥匙总数。

第K+3+J 行(1<=J<=S),有3个整数, 依次为Xi1,Yi1,Qi: 表示第J 把钥匙存放在(Xi1,Yi1)单元里,并且第J 把钥匙是用来开启第Qi类门的。 (其中 1<=Qi<=P) 。

输入数据中同一行各相邻整数之间用一个空格分隔。

输出格式

程序运行结束时,将麦克营救到大兵瑞恩的最短时间的值输出。如果问题无解,则输出-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

提示

存在一个格子有多把钥匙的情况


思路

此题要用状态压缩+BFS

我们可以状态压缩钥匙

vis[x][y][S] 表示在(x,y)这个位置持有钥匙状态S是否来过

BFS队列里存4个变量

分别为当前横纵坐标步数以及钥匙状态

map[x1][y1][x2][y2]表示两个点之间是否有墙或者是门。

ys[x][y][i]表示(x,y)这个点放的钥匙的种类

num[x][y]表示当前点钥匙数目

坑点:

1.钥匙不是用了就没了((#`O′)喂,没了才有问题吧

2.一个点可以放多个钥匙(人家又没说只放一个。

3.初始点可以放钥匙

4.别忘了输出-1....

那么该怎么状态压缩呢?

什么是状态压缩?

状态压缩通俗的说就是把一个不可很难直接标记的状态变得更好标记。

(通过局部状态来枚举全局状态)

如何状态压缩?

以本题为例,题目当中钥匙可能有10种,按照我们之前的方法,需要开一个10维布尔数组来进行标记,但这无疑为判重带来了巨大的麻烦。于是我们观察每把钥匙的状态,发现钥匙只有两种状态还是没有。于是考虑二进制。用一个10位二进制数来表示每一把钥匙的状态,如0000000000就表示目前你一把钥匙都没有,而0000001000就表示你有第4把钥匙,那我们如何实现呢?

首先你先确保你明白二进制的运算后再看下文。

假如我们现在一把钥匙都没有,来到了一个有第4把钥匙风水宝地,我们就需要拿起这一把钥匙,使得二进制数0000000000变成0000001000,我们就只需要把数字1向左移动3个单位,下面画个图进行理解:

也就是说如果我们取了q 把钥匙,我们需要把它压入状态只需要将它向左移动 q−1 位。

那么如果说我们有多把钥匙呢?

那我们就考虑按位或,举个例子,如果你现在有了第三把钥匙,你又拿到了第四把,那么我们只需要把这两个二进制数按位或一下即可。

画个图:

也就是说我们将两把钥匙合并只需要按位或一下即可。

例如bfs的初始化的状态压缩的代码如下:

//初始化
  int t = 0;
  for(int i = 1; i <= num[1][1]; i++) t |= (1<<(ys[1][1][i] - 1));
//t是原来的钥匙状态,1<<(ys[1][1][i] - 1)是新来的。
  vis[1][1][t] = 1;

那么我们现在解决了拿钥匙的问题,现在我们要解决判重的问题,怎么解决呢?

这次我们用按位与,举个例子,如果你现在有了第2,3,4,7,9把钥匙,而这扇门需要第八把钥匙,我们只需要把你手中有的钥匙的状态第八把钥匙的状态按位与,如果结果为0,则没有这一把钥匙,否则就这把钥匙,画个图:

 

 

 但是你要注意,第 p 把钥匙的状态时二进制数左移 p−1 位的,不能搞错。

例如bfs中判重的代码:

for(int j = 1; j <= num[t.x][t.y]; j++)  cosx |= (1 << (ys[t.x][t.y][j] - 1));
if(vis[t.x][t.y][cosx]) continue;

还有判断目前移到的门拿没拿到钥匙:

int p;
if((p = mp[d.x][d.y][t.x][t.y]) != 0)
  if((d.cos & (1 << (p - 1))) == 0)
    continue;

这道题目好像难的就是状态压缩吧,其他你应该可以自己做出来的。


代码

#include <bits/stdc++.h>
#define int long long
using namespace std;
int n,m,p,k,s;
//迷宫有n行m列  迷宫里所有的门被分成p类  迷宫中门和墙的总数为k  迷宫中存放的钥匙总数为s
int num[20][20];//表示(x,y)这个点钥匙数量
int ys[20][20][10001];//表示(x,y)这个点放的钥匙
bool vis[20][20][10001];//vis[x][y][s]表示在(x,y)这个位置持有钥匙状态为s是否来过
int mp[20][20][20][20];//mp[x1][y1][x2][y2]表示两个点直接是否有墙或者是门
struct node
{
  int x,y,step,cos;//x,y,从起点到(x,y)的路径长度,钥匙状态
  node()
  {
    x = y = step = cos = 0;
  }
};
queue<node>q;
int dx[4] = {-1,0,1,0},dy[4] = {0,-1,0,1};
int bfs()
{
  //初始化
  int t = 0;
  for(int i = 1; i <= num[1][1]; i++) t |= (1<<(ys[1][1][i] - 1));
  vis[1][1][t] = 1;
  node d;
  d.x = 1;
  d.y = 1;
  d.step = 0;
  d.cos = t;
  q.push(d);
  while(!q.empty())
  {
    node d = q.front();
    q.pop();
    if(d.x == n && d.y == m) return d.step;
    for(int i = 0; i < 4; i++)
    {
      node t;
      t.x = d.x + dx[i];
      t.y = d.y + dy[i];
      if(t.x > 0 && t.x <= n && t.y > 0 && t.y <= m && mp[d.x][d.y][t.x][t.y] != -1)
      {
        int p;
        if((p = mp[d.x][d.y][t.x][t.y]) != 0)
          if((d.cos & (1 << (p - 1))) == 0)
            continue;
        int cosx = d.cos;
        for(int j = 1; j <= num[t.x][t.y]; j++)
          cosx |= (1 << (ys[t.x][t.y][j] - 1));
        if(vis[t.x][t.y][cosx]) continue;
        vis[t.x][t.y][cosx] = 1;
        t.step = d.step + 1;
        t.cos = cosx;
        q.push(t);
      }
    }
  }
  return -1;
}
signed main()
{
  scanf("%lld%lld%lld%lld",&n,&m,&p,&k);
  for(int i = 1; i <= k; i++)
  {
    int x_1,x_2,y_1,y_2,g;
    scanf("%lld%lld%lld%lld%lld",&x_1,&y_1,&x_2,&y_2,&g);
    if(g == 0) mp[x_1][y_1][x_2][y_2] = mp[x_2][y_2][x_1][y_1] = -1;
    else mp[x_1][y_1][x_2][y_2] = mp[x_2][y_2][x_1][y_1] = g;
  }
  scanf("%lld",&s);
  for(int i = 1; i <= s; i++)
  {
    int x,y,q;
    scanf("%lld%lld%lld",&x,&y,&q);
    num[x][y]++;
    ys[x][y][num[x][y]] = q;
  }
  printf("%lld",bfs());
  return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值