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,并不是数值的位置连续 问题: 给出一个未排序的整数数组,找出最长的连续元素序列的长度 ...
随机推荐
- 算法金 | 一个强大的算法模型,GP !!
大侠幸会,在下全网同名「算法金」 0 基础转 AI 上岸,多个算法赛 Top 「日更万日,让更多人享受智能乐趣」 高斯过程算法是一种强大的非参数机器学习方法,广泛应用于回归.分类和优化等任务中.其核心 ...
- email邮件(带附件,模拟文件上传,跨服务器)发送核心代码 Couldn't connect to host, port: smtp.163.com, 25; timeout -1;
邮件(带附件,模拟文件上传,跨服务器)发送核心代码1.测试邮件发送附件接口 /** * 测试邮件发送附件 * @param multipartFile * @return */ @RequestMap ...
- SVG <pattern> 标签的用法和应用场景
通过使用 <pattern> 标签,可以在 SVG 图像内部定义可重复使用的任意图案.这些图案可以通过 fill 属性或 stroke 属性进行引用. 使用场景 例如我们要在 <sv ...
- Built-in COM has been disabled via a feature switch.
.net 6.0 开始默认关闭com组件 使用时会出现以下信息 Built-in COM has been disabled via a feature switch. See https://aka ...
- 在Linux驱动中使用LED子系统
在Linux驱动中使用LED子系统 原文:https://blog.csdn.net/hanp_linux/article/details/79037684 前提配置device driver下面的L ...
- Python性能测试框架:Locust实战教程
01认识Locust Locust是一个比较容易上手的分布式用户负载测试工具.它旨在对网站(或其他系统)进行负载测试,并确定系统可以处理多少个并发用户,Locust 在英文中是 蝗虫 的意思:作者的想 ...
- VS2015 、VS2017 MFC输出日志到控制台窗口
原来使用VS2010建立的项目,安装VS2017后,发现MFC无法通过调试窗口输出printf打印的内容,在CSDN上找到了一个解决方案,使用后恢复打印调试信息功能,推荐如下: https://blo ...
- node.js (原生模板引擎模板)
app01 // 引入http模块 const http = require('http'); //连接数据库 require('./model/connects'); // 创建网站服务器 cons ...
- mac 安装homebrew 报443
描述 macOS安装Homebrew时总是报错(Failed to connect to raw.githubusercontent.com port 443: Connection refused) ...
- SpringCloud连接远程nacos报错,一直提示连接本地的localhost:8848
application.properties spring.cloud.nacos.discovery.server-addr=xxx.xxx.xxx.xxx:8848 spring.applicat ...