POI1999 Store-keeper 题解
前言
题目链接:洛谷;SPOJ;hydro & bzoj。
\(\Theta(nm)\) 的算法。
题意简述
在一个划分为 \(n \times m\) 个区域的二维仓库中,称有公共边的两个区域为相邻的。
初始你在地图 M 的位置,要把包裹从 P 区域运到 K 区域。你在移动时,只能前往相邻的区域。当你站在包裹相邻的区域时,往包裹的方向前进,可以推动包裹,包裹也会向那个方向前进。
移动时,你和包裹都不能经过 S 位置。其余 w 是空位置。
请问:你最少推多少次包裹就能把它运到终点。如果不能,输出 NO。
输入包含多组数据。
题目分析
朴素解法
不就是推箱子吗?状态只用记人的位置和箱子的位置即可。然后 BFS 搜索就行了。状态数是 \(\Theta(n^2m^2)\) 的,对于本题极限能过。注意到在判断状态是否曾经到达过时,把箱子的坐标放在前两维,使内存访问更加连续,能快一些。以及多测清空时,不是真正清空,而是把 bool 记成 int,并额外记一个时间戳,这样清空就是 \(\Theta(1)\) 的了。代码在这里,总时空分别为 \(6.92\) 秒、\(422.00\) MB。到此结束。
更优解法
然而,本题有更优的 \(\Theta(nm)\) 的算法。
发现,我们只关心推箱子的次数,那么箱子在原地,人在外面瞎跑是很冗余的状态。考虑压缩状态,我们只记录箱子的位置,以及人紧贴在箱子的哪一侧。即用 \((x, y, 0/1/2/3)\) 的三元组来刻画一个局面。
初始情况,我们用一次 BFS,标记出,人在不推箱子的情况下能到达的点,即把箱子也看做是一个障碍物。然后看看箱子上下左右四个点,哪些可以到达。能到达的点,就是初始状态。
对于一个状态,我们考虑进行一步,有如下两种情况:
- 箱子被人推动了一步。
需要判断箱子是否越界之类的问题。很 naive,可以参考以上朴素解法。然后让步数加 \(1\)。 - 人从箱子的一侧走到了另一侧。
这是我们接下来要着重讨论的。
如果,就是说如果,每个状态都跑一遍 \(\Theta(nm)\) 的 BFS,然后标记能到达的点,那么时间复杂度又回去了,考虑优化。
尝试对此问题抽象。把原图相邻的空地连一条无向边,只考虑有解情况下,箱子和人能到达的位置组成的图是一张连通的无向图。问题成为了:若图中存在 \(u \leftrightarrow yzh \leftrightarrow v\),那么,\(u\) 和 \(v\) 在不经过 \(yzh\) 的情况下是否是连通的?
删去一个点,问两个点是否是连通的,自然地想到 tarjan 求无向图的点双连通分量。
发现上述问题又变成了询问 \(u\) 和 \(v\) 是否处在同一个点双连通分量里面。为什么?考虑到存在 \(u \leftrightarrow yzh \leftrightarrow v\) 的特殊性,倘若 \(u\) 和 \(v\) 存在两个点双连通分量里,而除了经过 \(yzh\),还能从 \(xym\) 互相到达,那么就会出现矛盾:\(u\) 和 \(v\) 此时就存在同一个点双连通分量里了。再换一句话来说,如果 \(u\) 和 \(v\) 处在两个点双连通分量里,它们除了经过 \(yzh\) 一定不能互相到达,\(yzh\) 一定是一个割点。
那么如何判断两点是否处在同一个点双连通分量里呢?在弹栈的时候标记在同一个点双连通分量里,仅此而已?注意到,一个割点是同时出现在多个点双连通分量里的,它可能被标记了多次。用 vector 记每点可能存在的分量,然后判断的时候暴力枚举判断?这样就怕时间复杂度会假。我们希望是 \(\Theta(1)\) 地判断。
考虑 tarjan 的 DFS 树。以下简称点双连通分量为分量。在弹栈的时候,总是找到一个割点,然后把子树还在栈里的都标记在同一个分量里,这个割点还留在栈里,等回溯到更浅的时候被重新标记在另一个分量中。如果分量里一个点被重新标记为另一个分量里,一定是这一个来弹栈的点。所以,我们把每个分量里,用来弹栈的割点,也就是可能被标记为另一个分量的点,记录下来。判断的时候,先判断 \(u\) 和 \(v\) 是否被标记在同一个分量里,若不是,在判断 \(u\) 所在分量里弹栈的点是否是 \(v\),\(v\) 同理。
这样,就能轻松判断了,tarjan 的时间复杂度是 \(\Theta(nm)\) 的。跑得有点远了?
BFS 的时候,每次步数均不变或加一,不需要跑最短路,用 01-BFS 即可,时间复杂度是 \(\Theta(nm)\) 的。如果你是用双端队列 deque,那么常数可能会很大。我用两个队列,Q[0/1] 来模拟。每次在 Q[0] 中取 front,若 Q[0] 为空,交换 Q[0] 和 Q[1]。push 的时候,若步数不变,则加到 Q[0] 里,否则加到 Q[1] 里。这是 01-BFS 的小 trick 吧。
总的时间复杂度是 \(\Theta(nm)\) 的。看了看洛谷题解区,是最优的了。相比另一位大佬想到了 tarjan,但是树上倍增多了一只 \(\log\),这种方法更优。
代码
写了注释,马蜂良好,供大家学习参考。耗时才 \(10\) 毫秒左右,浪漫内存 \(5.20\) MB。
// #pragma GCC optimize(3)
// #pragma GCC optimize("Ofast", "inline", "-ffast-math")
// #pragma GCC target("avx", "sse2", "sse3", "sse4", "mmx")
#include <cstdio>
#include <queue>
#include <cstring>
using namespace std;
const int mov[][2] = {0, 1, 0, -1, 1, 0, -1, 0};
int n, m;
char world[101][101];
int mx, my;
int px, py;
int kx, ky;
int scc_cnt, top;
int dfn[101][101], low[101][101], timer;
int bl[101][101]; // 每个点所在点双连通分量的编号
pair<int, int> stack[101 * 101];
pair<int, int> pts[101 * 101]; // 每个点双连通分量用来弹栈的点
void tarjan(int x, int y, int fx, int fy){
dfn[x][y] = low[x][y] = ++timer, stack[++top] = {x, y};
int son = 0;
for (int i = 0; i < 4; ++i) {
int tx = x + mov[i][0], ty = y + mov[i][1];
if (tx < 1 || tx > n || ty < 1 || ty > m) continue;
if (world[tx][ty] == 'S') continue;
if (!dfn[tx][ty]){
tarjan(tx, ty, x, y), low[x][y] = min(low[x][y], low[tx][ty]), ++son;
if (low[tx][ty] >= dfn[x][y]){
pts[bl[x][y] = ++scc_cnt] = {x, y}; // 这个点是新的分量的用来弹栈的点
while (true) {
auto [xx, yy] = stack[top--];
bl[xx][yy] = scc_cnt;
if (xx == tx && yy == ty)
break;
}
}
} else if (tx != fx || ty != fy) // 求割点这句话可有可无
low[x][y] = min(low[x][y], dfn[tx][ty]);
}
if (!son && !fx && !fy) bl[x][y] = ++scc_cnt; // 这句话可有可无,因为我们有解情况下,状态集中不会有孤立点
}
bool mcan[101][101]; // 人从初始点不推箱子能到达的位置
void prebfs() {
memset(mcan, 0x00, sizeof mcan);
mcan[px][py] = true; // 这样就不能经过箱子了
queue<pair<int, int>> Q;
Q.push({mx, my}), mcan[mx][my] = true;
while (!Q.empty()) {
auto [x, y] = Q.front(); Q.pop();
for (int i = 0; i < 4; ++i) {
int tx = x + mov[i][0], ty = y + mov[i][1];
if (tx < 1 || tx > n || ty < 1 || ty > m) continue;
if (world[tx][ty] == 'S') continue;
if (mcan[tx][ty]) continue;
mcan[tx][ty] = true;
Q.push({tx, ty});
}
}
}
struct node {
int x, y, dir; // 箱子的位置,以及人在箱子的哪个方向
};
int f[101][101][4];
void solve() {
scanf("%d%d", &n, &m), timer = scc_cnt = top = 0;
for (int i = 1; i <= n; ++i) {
scanf("%s", world[i] + 1);
for (int j = 1; j <= m; ++j) {
dfn[i][j] = low[i][j] = bl[i][j] = 0;
if (world[i][j] == 'M') {
mx = i, my = j;
} else if (world[i][j] == 'P') {
px = i, py = j;
} else if (world[i][j] == 'K') {
kx = i, ky = j;
}
}
}
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
if (world[i][j] != 'S' && !dfn[i][j]) {
tarjan(i, j, 0, 0);
}
prebfs();
queue<node> Q[2];
memset(f, 0xff, sizeof f); // 用 -1 标记没有访问过
for (int i = 0; i < 4; ++i) {
int tx = px + mov[i][0], ty = py + mov[i][1];
if (tx < 1 || tx > n || ty < 1 || ty > m) continue;
if (!mcan[tx][ty]) continue;
Q[0].push({px, py, i}); // 初始情况
f[px][py][i] = 0;
}
while (!Q[0].empty() ||!Q[1].empty()) {
if (Q[0].empty()) swap(Q[0], Q[1]);
auto [x, y, dir] = Q[0].front(); Q[0].pop(); // 01-BFS
if (x == kx && y == ky) {
printf("%d\n", f[x][y][dir]);
return;
}
int xx = x - mov[dir][0], yy = y - mov[dir][1]; // (xx, yy) 是箱子如果被推了,到达的位置
if (1 <= xx && xx <= n && 1 <= yy && yy <= m && world[xx][yy] != 'S' && !~f[xx][yy][dir]) {
f[xx][yy][dir] = f[x][y][dir] + 1;
Q[1].push({xx, yy, dir});
}
xx = x + mov[dir][0], yy = y + mov[dir][1]; // (xx, yy) 是人的位置
for (int i = 0; i < 4; ++i) if (i != dir) {
int tx = x + mov[i][0], ty = y + mov[i][1]; // (tx, ty) 是箱子 i 方向上的点
if (tx < 1 || tx > n || ty < 1 || ty > m) continue;
if (world[tx][ty] == 'S') continue;
if (!~f[x][y][i] && (bl[xx][yy] == bl[tx][ty] || pts[bl[xx][yy]] == pair<int, int>{ tx, ty } ||
pts[bl[tx][ty]] == pair<int, int>{ xx, yy })) { // 对应题解中,判断人能不能从 dir 变成 i 这个方向
f[x][y][i] = f[x][y][dir];
Q[0].push({x, y, i});
}
}
}
puts("NO");
}
signed main() {
int t; scanf("%d", &t);
while (t--) solve();
return 0;
}
后记
本方法在传统爆搜基础上,压缩了状态,简化了状态间可不可达的判断,非常值得借鉴,对我们思考问题也有诸多启示。
POI1999 Store-keeper 题解的更多相关文章
- BZOJ2802Warehouse Store题解
链接 我太菜了,连贪心题都不会写... 贪心思路很简单,我们能满足顾客就满足他,如果满足不了,就看之前的顾客中 有没有需求比该顾客多的顾客,如果有的话改为卖给这位顾客会使解更优 所以我们用一个优先队列 ...
- 大家AK杯 灰天飞雁NOIP模拟赛题解/数据/标程
数据 http://files.cnblogs.com/htfy/data.zip 简要题解 桌球碰撞 纯模拟,注意一开始就在袋口和v=0的情况.v和坐标可以是小数.为保险起见最好用extended/ ...
- BZOJ2802: [Poi2012]Warehouse Store
2802: [Poi2012]Warehouse Store Time Limit: 10 Sec Memory Limit: 64 MBSec Special JudgeSubmit: 121 ...
- 【LeetCode题解】二叉树的遍历
我准备开始一个新系列[LeetCode题解],用来记录刷LeetCode题,顺便复习一下数据结构与算法. 1. 二叉树 二叉树(binary tree)是一种极为普遍的数据结构,树的每一个节点最多只有 ...
- Codeforces Round #257 (Div. 1)A~C(DIV.2-C~E)题解
今天老师(orz sansirowaltz)让我们做了很久之前的一场Codeforces Round #257 (Div. 1),这里给出A~C的题解,对应DIV2的C~E. A.Jzzhu and ...
- 2929: [Poi1999]洞穴攀行
2929: [Poi1999]洞穴攀行 Time Limit: 1 Sec Memory Limit: 128 MBSubmit: 80 Solved: 41[Submit][Status][Di ...
- usaco training 4.1.2 Fence Rails 题解
Fence Rails题解 Burch, Kolstad, and Schrijvers Farmer John is trying to erect a fence around part of h ...
- 【LeetCode题解】225_用队列实现栈(Implement-Stack-using-Queues)
目录 描述 解法一:双队列,入快出慢 思路 入栈(push) 出栈(pop) 查看栈顶元素(peek) 是否为空(empty) Java 实现 Python 实现 解法二:双队列,入慢出快 思路 入栈 ...
- 如何删除mac keeper
如果不小心安装了mac keeper,基本是无法删除的,而且16年以前的方法都不管用.可以这样删除,我已经测试过了,下载https://data-cdn.mbamupdates.com/web/mba ...
- leetcode & lintcode 题解
刷题备忘录,for bug-free 招行面试题--求无序数组最长连续序列的长度,这里连续指的是值连续--间隔为1,并不是数值的位置连续 问题: 给出一个未排序的整数数组,找出最长的连续元素序列的长度 ...
随机推荐
- Azure Storage Blob 启用sftp协议支持
背景 我这边需要给前端同学一个上传静态文件的地方,比如js.css.图片.icons等等,前端上传后直接在项目中:我这边用的是Azure Storage blob:为了单独分配权限,我这边打算启用SF ...
- python 使用pandas修改数据到excel,报“SettingwithCopyWarning A value is trying to be set on a copy of a slice from a DataFrame”的解决方法
场景: 通过pandas模块,将测试数据回写到excel,测试数据有写到excel文件,但控制台输出警告信息如下 警告: SettingwithCopyWarning A value is tryin ...
- 2019南京区域赛ABCHJK题解 & KM-bfs(O(n^3))板子
A.Hard Problem 题目大意:给你一个数n,然后让你计算一个子集大小,这个大小的子集要保证一定存在一个数是另一个数的约数,求出这个最小的数. 做法:显然后面的\(\frac{n}{2}\)个 ...
- 日常Bug排查-改表时读数据不一致
前言 日常Bug排查系列都是一些简单Bug的排查.笔者将在这里介绍一些排查Bug的简单技巧,同时顺便积累素材. Bug现场 线上连续两天出现NP异常,而且都是凌晨低峰期才出现,在凌晨的流量远没有白天高 ...
- 在WPF中使用WriteableBitmap对接工业相机及常用操作
写作背景 写这篇文章主要是因为工业相机(海康.大恒等)提供的.NET开发文档和示例程序都是用WinForm项目来说明举例的,而在WPF项目中对图像的使用和处理与在WinForm项目中有很大不同.在Wi ...
- ETL服务器连接GaussDB(DWS)集群客户端配置
问题描述:给ETL的服务器上安装gsql的工具,用来连接GaussDB(DWS)集群,做数据抽取用 DWS:GaussDB(DWS) 8.2.1-ESL 1.获取软件包 登录FusionInsight ...
- MySQL - CASE WHEN的高级用法
Case语法 CASE WHEN condition1 THEN result1 WHEN condition2 THEN result2 WHEN conditionN THEN resultN E ...
- .Net Core 全局捕获异常-过滤器
1.代码版本 .Net Core 版本 2.2 2.自定义异常捕获类 新建一个类 CustomerExceptionFilter.cs 继承 Microsoft.AspNetCore.Mvc.Filt ...
- 类、事件与对象---Dad&Mom&Friends(进阶事件)
接上一个笔记:https://www.cnblogs.com/StephenYoung/p/17792668.html 现在增加了一个新的朋友类:Friends 这个类构造如下: 从上到下依次是: 1 ...
- Docker运维之容器的日志清理
在容器运行的过程中,通常会产生大量的日志,尤其是应用程序本身记录了info级别的日志时候,程序的标准输出记录到容器的日志.这样会占用大量的磁盘空间,严重者导致IO异常,最终服务会宕机. 方案一:定期手 ...