BFS(双端队列+状态压缩) - 拯救大兵瑞恩 - HDU 4845
题意:
给 定 n × m 的 迷 宫 , 起 点 在 左 上 角 ( 1 , 1 ) , 终 点 在 右 下 角 ( n , m ) 。 给定n×m的迷宫,起点在左上角(1,1),终点在右下角(n,m)。 给定n×m的迷宫,起点在左上角(1,1),终点在右下角(n,m)。
两 个 点 之 间 有 三 种 连 通 方 式 : 墙 ( 不 连 通 ) , 门 ( 需 要 钥 匙 才 能 通 过 ) , 直 接 连 通 。 两个点之间有三种连通方式:墙(不连通),门(需要钥匙才能通过),直接连通。 两个点之间有三种连通方式:墙(不连通),门(需要钥匙才能通过),直接连通。
首 行 输 入 三 个 整 数 n , m , p , n 和 m 表 示 迷 宫 的 行 和 列 , p 是 冗 余 条 件 。 首行输入三个整数n,m,p,n和m表示迷宫的行和列,p是冗余条件。 首行输入三个整数n,m,p,n和m表示迷宫的行和列,p是冗余条件。
再 输 入 一 行 整 数 k , 表 示 门 和 墙 的 总 数 , 接 着 输 入 k 行 数 据 , 每 行 包 括 两 个 坐 标 ( x 1 , y 1 ) , ( x 2 , y 2 ) 和 c , 再输入一行整数k,表示门和墙的总数,接着输入k行数据,每行包括两个坐标(x_1,y_1),(x_2,y_2)和c, 再输入一行整数k,表示门和墙的总数,接着输入k行数据,每行包括两个坐标(x1,y1),(x2,y2)和c,
c = 0 时 , 表 示 ( x 1 , y 1 ) , ( x 2 , y 2 ) 之 间 有 墙 , 即 不 连 通 。 c=0时,表示(x_1,y_1),(x_2,y_2)之间有墙,即不连通。 c=0时,表示(x1,y1),(x2,y2)之间有墙,即不连通。
c > 0 时 , 表 示 ( x 1 , y 1 ) , ( x 2 , y 2 ) 之 间 有 门 , c 为 门 的 编 号 ( 与 钥 匙 要 对 应 ) 。 c>0时,表示(x_1,y_1),(x_2,y_2)之间有门,c为门的编号(与钥匙要对应)。 c>0时,表示(x1,y1),(x2,y2)之间有门,c为门的编号(与钥匙要对应)。
然 后 输 入 整 数 s , 表 示 钥 匙 的 数 量 , 最 后 输 入 s 行 数 据 , 然后输入整数s,表示钥匙的数量,最后输入s行数据, 然后输入整数s,表示钥匙的数量,最后输入s行数据,
每 行 包 括 x , y , q , 表 示 在 ( x , y ) 处 有 q 号 门 的 钥 匙 。 每行包括x,y,q,表示在(x,y)处有q号门的钥匙。 每行包括x,y,q,表示在(x,y)处有q号门的钥匙。
现 要 求 从 左 上 角 到 右 下 角 的 最 短 距 离 。 现要求从左上角到右下角的最短距离。 现要求从左上角到右下角的最短距离。
Sample Input
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
Sample Output
14
样
例
如
下
图
:
样例如下图:
样例如下图:
数据范围:
1 ≤ n , m , p ≤ 10 , ∣ x 1 − x 2 ∣ + ∣ y 1 − y 2 ∣ = 1 , 0 ≤ c ≤ p , 1 ≤ q ≤ p , 1 ≤ k ≤ 150 T i m e l i m i t : 1000 m s , M e m o r y l i m i t : 32768 k B 1≤n,m,p≤10,|x_1−x_2|+|y_1−y_2|=1,0≤c≤p,1≤q≤p,1≤k≤150\\Time \ limit:1000 ms,Memory \ limit:32768 kB 1≤n,m,p≤10,∣x1−x2∣+∣y1−y2∣=1,0≤c≤p,1≤q≤p,1≤k≤150Time limit:1000ms,Memory limit:32768kB
分析:
若 没 有 钥 匙 限 制 , 可 以 直 接 b f s 得 到 最 短 路 。 若没有钥匙限制,可以直接bfs得到最短路。 若没有钥匙限制,可以直接bfs得到最短路。
加 上 钥 匙 的 限 制 , 我 们 需 要 拆 点 , 进 行 状 态 压 缩 。 加上钥匙的限制,我们需要拆点,进行状态压缩。 加上钥匙的限制,我们需要拆点,进行状态压缩。
由 于 钥 匙 的 数 量 不 超 过 10 , 故 我 们 用 一 个 长 度 为 10 的 二 进 制 数 来 表 示 当 前 位 置 有 哪 些 编 号 的 钥 匙 。 由于钥匙的数量不超过10,故我们用一个长度为10的二进制数来表示当前位置有哪些编号的钥匙。 由于钥匙的数量不超过10,故我们用一个长度为10的二进制数来表示当前位置有哪些编号的钥匙。
为 了 方 便 , 我 们 将 二 维 坐 标 映 射 到 一 维 坐 标 , 每 个 点 ( x , y ) 给 一 个 编 号 i d 。 为了方便,我们将二维坐标映射到一维坐标,每个点(x,y)给一个编号id。 为了方便,我们将二维坐标映射到一维坐标,每个点(x,y)给一个编号id。
距 离 数 组 d i s [ i ] [ s t a t e ] 表 示 从 源 点 到 点 i 且 当 前 拥 有 的 钥 匙 为 s t a t e 的 最 短 距 离 。 距离数组dis[i][state]表示从源点到点i且当前拥有的钥匙为state的最短距离。 距离数组dis[i][state]表示从源点到点i且当前拥有的钥匙为state的最短距离。
记 当 前 点 的 编 号 为 i d , 状 态 为 c u r , 与 之 相 连 通 的 点 的 编 号 为 j , 则 分 两 种 情 况 : 记当前点的编号为id,状态为cur,与之相连通的点的编号为j,则分两种情况: 记当前点的编号为id,状态为cur,与之相连通的点的编号为j,则分两种情况:
① 、 当 前 位 置 有 钥 匙 k e y [ i d ] : 最 优 解 一 定 是 拿 起 钥 匙 , 花 费 为 0 , 则 状 态 变 为 s t a t e = c u r ∣ k e y [ i d ] , ①、当前位置有钥匙key[id]:最优解一定是拿起钥匙,花费为0,则状态变为\ state=cur\ |\ key[id], ①、当前位置有钥匙key[id]:最优解一定是拿起钥匙,花费为0,则状态变为 state=cur ∣ key[id],
即 d i s [ j ] [ s t a t e ] = d i s [ i d ] [ c u r ] 。 \qquad即dis[j][state]=dis[id][cur]。 即dis[j][state]=dis[id][cur]。
② 、 向 周 围 四 个 方 向 扩 展 ( 直 接 连 通 或 者 有 门 且 有 对 应 的 钥 匙 ) : ②、向周围四个方向扩展(直接连通或者有门且有对应的钥匙): ②、向周围四个方向扩展(直接连通或者有门且有对应的钥匙):
判 断 能 否 开 门 : 当 前 位 置 的 门 的 编 号 w [ i ] > 0 , 表 示 有 门 , 若 状 态 c u r 的 对 应 的 位 置 上 不 为 1 , 表 示 没 钥 匙 , 不 连 通 。 \qquad判断能否开门:当前位置的门的编号w[i]>0,表示有门,\\\qquad若状态cur的对应的位置上不为1,表示没钥匙,不连通。 判断能否开门:当前位置的门的编号w[i]>0,表示有门,若状态cur的对应的位置上不为1,表示没钥匙,不连通。
否 则 两 点 连 通 , 判 断 通 过 i d 再 到 j 是 否 更 近 : 若 d i s [ j ] > d i s [ i d ] + 1 , 则 更 新 d i s [ j ] = d i s [ i d ] + 1 , 将 j 入 队 继 续 扩 展 。 \qquad否则两点连通,判断通过id再到j是否更近:\\\qquad若dis[j]>dis[id]+1,则更新dis[j]=dis[id]+1,将j入队继续扩展。 否则两点连通,判断通过id再到j是否更近:若dis[j]>dis[id]+1,则更新dis[j]=dis[id]+1,将j入队继续扩展。
建图:
本 题 建 图 也 有 难 度 。 本题建图也有难度。 本题建图也有难度。
首 先 我 们 将 n × m 的 矩 阵 中 的 所 有 点 依 次 编 号 , 映 射 到 一 维 坐 标 上 来 。 首先我们将n×m的矩阵中的所有点依次编号,映射到一维坐标上来。 首先我们将n×m的矩阵中的所有点依次编号,映射到一维坐标上来。
接 着 我 们 根 据 读 入 的 门 和 墙 先 建 立 一 条 无 向 边 , 并 将 该 边 加 入 到 集 合 中 去 。 接着我们根据读入的门和墙先建立一条无向边,并将该边加入到集合中去。 接着我们根据读入的门和墙先建立一条无向边,并将该边加入到集合中去。
然 后 我 们 遍 历 整 个 图 , 通 过 集 合 判 断 , 将 相 邻 之 间 没 有 边 的 两 点 之 间 建 立 一 条 边 , 权 值 置 为 0 , 表 示 连 通 。 然后我们遍历整个图,通过集合判断,将相邻之间没有边的两点之间建立一条边,权值置为0,表示连通。 然后我们遍历整个图,通过集合判断,将相邻之间没有边的两点之间建立一条边,权值置为0,表示连通。
求解最短路:
由 于 边 权 仅 存 在 0 ( 拿 钥 匙 ) 和 1 ( 移 动 位 置 ) 两 种 情 况 , 故 我 们 可 以 用 双 端 队 列 广 搜 来 解 决 , 时 间 复 杂 度 是 线 性 的 。 由于边权仅存在0(拿钥匙)和1(移动位置)两种情况,故我们可以用双端队列广搜来解决,时间复杂度是线性的。 由于边权仅存在0(拿钥匙)和1(移动位置)两种情况,故我们可以用双端队列广搜来解决,时间复杂度是线性的。
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<deque>
#include<set>
#define P pair<int,int>
#define x first
#define y second
using namespace std;
const int N=11, M=N*N, E=N*(N-1)*2*2, S=1<<10;
int n,m,p,k;
int h[M],e[E],ne[E],w[E],idx;
int g[N][N];
int dis[M][S],key[M];
bool st[M][S];
set<P> edge;
void add(int a,int b,int c)
{
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
void build()
{
int dir[4][2]={{0,1},{0,-1},{1,0},{-1,0}};
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
for(int u=0;u<4;u++)
{
int x=i+dir[u][0], y=j+dir[u][1];
if(x<1||x>n||y<1||y>m) continue;
int a=g[i][j],b=g[x][y];
if(!edge.count({a,b})) add(a,b,0); //不存在时再建边
}
}
int bfs()
{
memset(dis,0x3f,sizeof dis);
dis[1][0]=0;
deque<P> dq;
dq.push_front({1,0}); //first:编号,second:状态
while(dq.size())
{
P t=dq.front();
dq.pop_front();
if(t.x==n*m) return dis[t.x][t.y];
int id=t.x,cur=t.y;
if(st[id][cur]) continue;
st[id][cur]=true;
if(key[id]) //当前位置有钥匙,拿起来
{
int state=cur|key[id];
dis[id][state]=dis[id][cur];
dq.push_front({id,state});
}
for(int i=h[id];~i;i=ne[i]) //向相邻方向扩展
{
int j=e[i];
if(w[i] && !( cur>>w[i]-1 & 1)) continue; //有门但没钥匙
if(dis[j][cur]>dis[id][cur]+1)
{
dis[j][cur]=dis[id][cur]+1;
dq.push_back({j,cur});
}
}
}
return -1;
}
int main()
{
cin>>n>>m>>p>>k;
for(int i=1,cnt=1;i<=n;i++)
for(int j=1;j<=m;j++)
g[i][j]=cnt++;
memset(h,-1,sizeof h);
while(k--)
{
int x1,y1,x2,y2,t;
cin>>x1>>y1>>x2>>y2>>t;
int a=g[x1][y1],b=g[x2][y2];
edge.insert({a,b}),edge.insert({b,a});
if(t) add(a,b,t),add(b,a,t);
}
int kcnt;
cin>>kcnt;
while(kcnt--)
{
int x,y,t;
cin>>x>>y>>t;
key[g[x][y]] |= 1<<(t-1); //映射为一个二进制数
}
build();
cout<<bfs()<<endl;
return 0;
}