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,并不是数值的位置连续 问题: 给出一个未排序的整数数组,找出最长的连续元素序列的长度 ...
随机推荐
- 如何解决Win10删除文件慢的办法
问题:最近使用KMS激活了一些工具,今天删除不需要的文件时发现删除文件很慢很慢,删除一个几百k的文件都很慢. 解决办法通过控制面板→管理工具→服务→找到该进程并设为禁用就OK了.
- 安卓app 地铁最短路径查询 完成
我通过三个函数 完成了这个功能 首先 创建哈希表 根据起始站名 终点站名 然后 根据哈希表 建立起 邻接表' 最后 根据迪杰斯特拉算法 完成这个功能 /** * function:起终查询 */ / ...
- JAVA-poi导出excel到http响应流
导出结果为excel是相对常见的业务需求,大部分情况下只需要导出简单的格式即可,所以有许多可以采用的方案.有些方案还是很容易实现的. 一.可用的解决方案 目前可以有几类解决方案: 字处理企业提供的解决 ...
- gcc系列工具 介绍
编译器相关知识学习 GNU GCC简介 GNU GCC是一套面向嵌入式领域的交叉编译工具,支持多种编程语言.多种优化选项并且能够支持分步编译.支持多种反汇编方式.支持多种调试信息格式,目前支持X86. ...
- 在WPF中使用WriteableBitmap对接工业相机及常用操作
写作背景 写这篇文章主要是因为工业相机(海康.大恒等)提供的.NET开发文档和示例程序都是用WinForm项目来说明举例的,而在WPF项目中对图像的使用和处理与在WinForm项目中有很大不同.在Wi ...
- 2-SET详解
前置知识 SET问题的标准定义:在计算机科学中,布尔可满足性问题(有时称为命题可满足性问题,缩写为SATISFIABILITY或SAT)是确定是否存在满足给定布尔公式的解释的问题.(全是废话) 说人话 ...
- 『vulnhub系列』Dripping-Blues-1
『vulnhub系列』Dripping-Blues-1 下载地址: https://www.vulnhub.com/entry/dripping-blues-1,744/ 信息搜集: 使用nmap进行 ...
- 3562-Qt工程编译说明、GPU核心使用说明
- 写sql语句思路--28道关于教师、学生、成绩表的练习题---个人思路
针对学生教师的28道练习题的思路 链接:https://pan.baidu.com/s/1TgqFAe7i0PAkZOm47-Jd0A 提取码:vvi6 部分截图如下: -- sql 28道练习题答案 ...
- 韦东山freeRTOS系列教程之【第五章】队列(queue)
目录 系列教程总目录 概述 5.1 队列的特性 5.1.1 常规操作 5.1.2 传输数据的两种方法 5.1.3 队列的阻塞访问 5.2 队列函数 5.2.1 创建 5.2.2 复位 5.2.3 删除 ...