1131. 拯救大兵瑞恩
1944 年,特种兵麦克接到国防部的命令,要求立即赶赴太平洋上的一个孤岛,营救被敌军俘虏的大兵瑞恩。
瑞恩被关押在一个迷宫里,迷宫地形复杂,但幸好麦克得到了迷宫的地形图。
迷宫的外形是一个长方形,其南北方向被划分为 NN 行,东西方向被划分为 MM 列, 于是整个迷宫被划分为 N×MN×M 个单元。
每一个单元的位置可用一个有序数对 (单元的行号, 单元的列号) 来表示。
南北或东西方向相邻的 22 个单元之间可能互通,也可能有一扇锁着的门,或者是一堵不可逾越的墙。
注意: 门可以从两个方向穿过,即可以看成一条无向边。
迷宫中有一些单元存放着钥匙,同一个单元可能存放 多把钥匙,并且所有的门被分成 PP 类,打开同一类的门的钥匙相同,不同类门的钥匙不同。
大兵瑞恩被关押在迷宫的东南角,即 (N,M)(N,M) 单元里,并已经昏迷。
迷宫只有一个入口,在西北角。
也就是说,麦克可以直接进入 (1,1)(1,1) 单元。
另外,麦克从一个单元移动到另一个相邻单元的时间为 11,拿取所在单元的钥匙的时间以及用钥匙开门的时间可忽略不计。
试设计一个算法,帮助麦克以最快的方式到达瑞恩所在单元,营救大兵瑞恩。
输入格式
第一行有三个整数,分别表示 N,M,PN,M,P 的值。
第二行是一个整数 kk,表示迷宫中门和墙的总数。
接下来 kk 行,每行包含五个整数,Xi1,Yi1,Xi2,Yi2,GiXi1,Yi1,Xi2,Yi2,Gi:当 Gi≥1Gi≥1 时,表示 (Xi1,Yi1)(Xi1,Yi1) 单元与 (Xi2,Yi2)(Xi2,Yi2) 单元之间有一扇第 GiGi 类的门,当 Gi=0Gi=0 时,表示 (Xi1,Yi1)(Xi1,Yi1) 单元与 (Xi2,Yi2)(Xi2,Yi2) 单元之间有一面不可逾越的墙。
接下来一行,包含一个整数 SS,表示迷宫中存放的钥匙的总数。
接下来 SS 行,每行包含三个整数 Xi1,Yi1,QiXi1,Yi1,Qi,表示 (Xi1,Yi1)(Xi1,Yi1) 单元里存在一个能开启第 QiQi 类门的钥匙。
输出格式
输出麦克营救到大兵瑞恩的最短时间。
如果问题无解,则输出 -1。
数据范围
|Xi1−Xi2|+|Yi1−Yi2|=1|Xi1−Xi2|+|Yi1−Yi2|=1,
0≤Gi≤P0≤Gi≤P,
1≤Qi≤P1≤Qi≤P,
1≤N,M,P≤101≤N,M,P≤10,
1≤k≤1501≤k≤150
输入样例:
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
输出样例:
14
样例解释:
迷宫如下所示:
很明显,在普通的最短路层次上面,发生了转移的限制,
比如,只有拿到钥匙才能打开门 。
所以要增加状态,
对st[],和dis[]
就要一起增加一维度
变成 st[][],和dis[][]
其次:
队列里面存的元素,要包含状态state (因为队列里面存放最基本的就是 st[][]和dis[][]的所有维度, 但是像迪杰除了dis的所有下标还会多一个 dis[]的值)
可以这样想:
之前dis[]/st[]只有一个维度的时候,也就只有一个转移(点与点的转移)
现在dis[][]/st[][]有两个维度,也就会有两个转移 (除了点与点的转移,还有状态与状态的转移)
我们之前所学的 枚举四个方向的,那是单纯的 点与点之间的转移
而本题,会有两个转移,一个是点维度,一个是状态维度
1.第一个转移:第二维状态的转移
2.第二个转移:第一维点与点之间的转移
小结多状态问题:
1.deque队列里面存的元素会相应多状态
2.转移过程也会随着状态的增多而增多 (比如上面两个转移)
输入没有明显边权,那么边权如何产生?
本题属于不明显的边权,
我们把边权分析出来的原因是:点与点之间的关系。
点与点之间如果是门:则边权为1
点与点之间如果是墙:则没有边
点与点之间如果不是门也不是墙:则边权为0
注意:
该边权不能作为判定,加入双端队列队首还是队尾的条件
只能作为两个相邻点之间的关系。
真正的边权还要看 dis[]关于时间t的转移,
如果是+1的,那么就插入队首
否则插入队尾
做状态压缩的题目:
对位运算的把握很重要,
首先我们要知道:
1.左移<<和右移>>是优先级最low的
两个操作:①存入状态 ②在状态里面查找
2.搞清楚在状态压缩里面:什么时候要左移和右移?与怎么加入状态中?
首先先搞清楚 原始编号 和 原始编号进行左移后的值 的区别
我们如果要把 “编号” 加入到状态里面,
只能把 1<<编号 加进去 (也就是原始编号经过处理的左移 )
我假装给它一个 专有名词 :“处理后的原始编号”
而加进去有两种方式:
①state + (1<<编号) 加
②state|(1<<编号) 或
例如一下:key[]里面干脆不存原始编号了,就直接存处理过的编号:
3.搞清楚如何检验在一个状态中,某个编号表示的事物存不存在?
ans: state>>编号&1
小结:
对2,3可以发现 一个规律:
存入状态的时候 编号-1了,也就是 : 1<<id-1
那么从状态中查找的时候 编号也要-1,也就是 : t.y>>w[i]-1
//注释版代码
#include <bits/stdc++.h>
#define x first
#define y second
using namespace std;
typedef pair<int,int> PII;
/*
钥匙有十把,
1.如果对钥匙开状态大小的话应该是P=1<<10
2.对二维的点使用一维的编号,应该是M=N*N
3.计算所有的边数E :
有竖着的边,也有横着的边
n*(n-1)*2(竖着横着) *2(正向反向)
差不多是: 360
所以E开400
*/
const int N=11,M=N*N,P=1<<11,E=400;
int n,m,p;
int k; //表示墙和门的数量
/*
开边权w[]的原因,虽然题目中没有像以往的套路一样
输入就给我们边权
这里的边权隐藏在相邻点的关系中
边权为1:表示需要钥匙才能通过
边权为0:表示可以直接通过
*/
int e[E],ne[E],w[E],h[M],idx;
int g[N][N]; //存二维点的一维下标
int key[M];
int dis[M][P]; //第一维是点的一维下标,第二维是状态
bool st[M][P];
set<PII> edges;
/*
开set的原因,由于我们整个图的所有点与相邻
的点之间都要存边
而输入只会告诉我们部分的边 (门、墙这一类两个点之间的关系)
所以我们为了后续补上其他边,
就要有个能判定是否被存的
但是如果是墙的话,就直接塞入set,假装建了边
实际没有建边
*/
void add(int a,int b,int c)
{
e[idx]=b;
ne[idx]=h[a];
w[idx]=c;
h[a]=idx++;
}
void build()
{
int dx[4]={-1,0,1,0};
int dy[4]={0,1,0,-1};
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
for(int u=0;u<4;u++)
{
int x=i+dx[u],y=j+dy[u];
if(!x||x>n||!y||y>m) //出界
{
continue;
}
int a=g[i][j],b=g[x][y]; //把当前边的的左右端点编号提取出来
if(!edges.count({a,b})) //判断一下有没有在set里
{
add(a,b,0); //0的边权含义是不需要钥匙直接可以通过
}
}
}
}
int bfs()
{
memset(dis,0x3f,sizeof dis);
dis[1][0]=0; //当前还没有钥匙,所以第二维是0
deque<PII> q;
q.push_back({1,0});
while(q.size())
{
PII t=q.front(); //第一维是点的编号,第二维是state
q.pop_front();
if(st[t.x][t.y]) continue;
st[t.x][t.y]=true;
if(t.x==n*m)
{
return dis[t.x][t.y];
}
//第一个转移
if(key[t.x]) //如果当前点有钥匙
{
int state=t.y|key[t.x]; //第二维新的状态
if(dis[t.x][state]>dis[t.x][t.y])
{
dis[t.x][state]=dis[t.x][t.y];
q.push_front({t.x,state});
}
}
//第二个转移
for(int i=h[t.x];i!=-1;i=ne[i])
{
int j=e[i];
if(w[i]&&!(t.y>>(w[i]-1)&1)) continue;
/*如果当前边权是1(有门)
但是当前位置在状态中发现没有钥匙,
那么就要continue 掉
注意w[i]-1的原因是key[t]|=1<<id-1; 存入状态-1了,那么在状态中找也要把编号-1
*/
//剩下的情况要么是w[]为0直接通行,要么就是w[]为1但是有钥匙,总之dis[][]更新起来要+1
//此处把边权为1的加入对尾好像和w[]为0和1冲突了,毕竟意义上
//所以以后就把w[]当做两点间的关系吧
//真正的边权就看 dis是否是更新了加一的
if(dis[j][t.y]>dis[t.x][t.y]+1)
{
dis[j][t.y]=dis[t.x][t.y]+1;
q.push_back({j,t.y});
}
}
}
return -1;
}
int main()
{
cin>>n>>m>>p>>k;
/*读入k条边之前,把二维点都编上号
打表
*/
for(int i=1,t=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
g[i][j]=t++;
}
memset(h,-1,sizeof h);
while(k--)
{
int x1,y1,x2,y2,c; //(x1,y1) (x2,y2) 和一个类型c
cin>>x1>>y1>>x2>>y2>>c;
int a=g[x1][y1],b=g[x2][y2];
edges.insert({a,b}),edges.insert({b,a});
if(c) //如果不是墙
{
add(a,b,c);
add(b,a,c);
}
}
build(); //把其他未建立的边建起来
int s; //钥匙的数量
cin>>s;
while(s--)
{
int x,y,id;
cin>>x>>y>>id; //表示x,y处有钥匙
// int t=g[x][y]; //点的一维下标
// key[t]+=1<<id-1;
key[g[x][y]]|=1<<(id-1); //"-"的优先级 大于 "<<",这么做-1是为从0开始
}
cout<<bfs()<<endl;
return 0;
}
//无注释版代码
#include <cstring>
#include <iostream>
#include <algorithm>
#include <deque>
#include <set>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 11, M = 360, P = 1 << 10;
int n, m, k, p;
int h[N * N], e[M], w[M], ne[M], idx;
int g[N][N], key[N * N];
int dist[N * N][P];
bool st[N * N][P];
set<PII> edges;
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
void build()
{
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
for (int u = 0; u < 4; u ++ )
{
int x = i + dx[u], y = j + dy[u];
if (!x || x > n || !y || y > m) continue;
int a = g[i][j], b = g[x][y];
if (!edges.count({a, b})) add(a, b, 0);
}
}
int bfs()
{
memset(dist, 0x3f, sizeof dist);
dist[1][0] = 0;
deque<PII> q;
q.push_back({1, 0});
while (q.size())
{
PII t = q.front();
q.pop_front();
if (st[t.x][t.y]) continue;
st[t.x][t.y] = true;
if (t.x == n * m) return dist[t.x][t.y];
if (key[t.x])
{
int state = t.y | key[t.x];
if (dist[t.x][state] > dist[t.x][t.y])
{
dist[t.x][state] = dist[t.x][t.y];
q.push_front({t.x, state});
}
}
for (int i = h[t.x]; ~i; i = ne[i])
{
int j = e[i];
if (w[i] && !(t.y >> w[i] - 1 & 1)) continue; // 有门并且没有钥匙
if (dist[j][t.y] > dist[t.x][t.y] + 1)
{
dist[j][t.y] = dist[t.x][t.y] + 1;
q.push_back({j, t.y});
}
}
}
return -1;
}
int main()
{
cin >> n >> m >> p >> k;
for (int i = 1, t = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
g[i][j] = t ++ ;
memset(h, -1, sizeof h);
while (k -- )
{
int x1, y1, x2, y2, c;
cin >> x1 >> y1 >> x2 >> y2 >> c;
int a = g[x1][y1], b = g[x2][y2];
edges.insert({a, b}), edges.insert({b, a});
if (c) add(a, b, c), add(b, a, c);
}
build();
int s;
cin >> s;
while (s -- )
{
int x, y, c;
cin >> x >> y >> c;
key[g[x][y]] |= 1 << c - 1;
}
cout << bfs() << endl;
return 0;
}