前言

题目链接:洛谷SPOJhydro & 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,标记出,人在不推箱子的情况下能到达的点,即把箱子也看做是一个障碍物。然后看看箱子上下左右四个点,哪些可以到达。能到达的点,就是初始状态。

对于一个状态,我们考虑进行一步,有如下两种情况:

  1. 箱子被人推动了一步。

    需要判断箱子是否越界之类的问题。很 naive,可以参考以上朴素解法。然后让步数加 \(1\)。
  2. 人从箱子的一侧走到了另一侧。

    这是我们接下来要着重讨论的。

如果,就是说如果,每个状态都跑一遍 \(\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 题解的更多相关文章

  1. BZOJ2802Warehouse Store题解

    链接 我太菜了,连贪心题都不会写... 贪心思路很简单,我们能满足顾客就满足他,如果满足不了,就看之前的顾客中 有没有需求比该顾客多的顾客,如果有的话改为卖给这位顾客会使解更优 所以我们用一个优先队列 ...

  2. 大家AK杯 灰天飞雁NOIP模拟赛题解/数据/标程

    数据 http://files.cnblogs.com/htfy/data.zip 简要题解 桌球碰撞 纯模拟,注意一开始就在袋口和v=0的情况.v和坐标可以是小数.为保险起见最好用extended/ ...

  3. BZOJ2802: [Poi2012]Warehouse Store

    2802: [Poi2012]Warehouse Store Time Limit: 10 Sec  Memory Limit: 64 MBSec  Special JudgeSubmit: 121  ...

  4. 【LeetCode题解】二叉树的遍历

    我准备开始一个新系列[LeetCode题解],用来记录刷LeetCode题,顺便复习一下数据结构与算法. 1. 二叉树 二叉树(binary tree)是一种极为普遍的数据结构,树的每一个节点最多只有 ...

  5. 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 ...

  6. 2929: [Poi1999]洞穴攀行

    2929: [Poi1999]洞穴攀行 Time Limit: 1 Sec  Memory Limit: 128 MBSubmit: 80  Solved: 41[Submit][Status][Di ...

  7. 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 ...

  8. 【LeetCode题解】225_用队列实现栈(Implement-Stack-using-Queues)

    目录 描述 解法一:双队列,入快出慢 思路 入栈(push) 出栈(pop) 查看栈顶元素(peek) 是否为空(empty) Java 实现 Python 实现 解法二:双队列,入慢出快 思路 入栈 ...

  9. 如何删除mac keeper

    如果不小心安装了mac keeper,基本是无法删除的,而且16年以前的方法都不管用.可以这样删除,我已经测试过了,下载https://data-cdn.mbamupdates.com/web/mba ...

  10. leetcode & lintcode 题解

    刷题备忘录,for bug-free 招行面试题--求无序数组最长连续序列的长度,这里连续指的是值连续--间隔为1,并不是数值的位置连续 问题: 给出一个未排序的整数数组,找出最长的连续元素序列的长度 ...

随机推荐

  1. 异步任务处理注解方法@Async实现异步多线程

    异步任务处理注解方法@Async实现异步多线程 1.定义配置类,设置参数2.定义任务执行类3.执行Spring 中的ThreadPoolExecutor是借助JDK并发包中的java.util.con ...

  2. Java正则表达式语法及简单示例

    import java.util.regex.Matcher; import java.util.regex.Pattern; public class TestMatcher { public st ...

  3. VMware 17 Exception 0xc0000094 解决

    VMWare16的虚拟机升级到17时, 可能会出现虚拟机可以正常使用, 但编辑设置就会出现vmui错误的现像. VMware Workstation unrecoverable error: (vmu ...

  4. 面试官:transient关键字修饰的变量当真不可序列化?我:烦请先生教我!

    一.写在开头 在这篇文章中记录一下之前自己面试时学到的东西,是关于transient关键字的,当时面试官问我IO的相关问题,基本上全答出来了,关于如何不序列化对象中某个字段时,我果断的选择了stati ...

  5. 【建议收藏】Go语言关键知识点总结

    容器 数组和切片 在Go语言中,数组和切片是两个基本的数据结构,用于存储和操作一组元素.它们有一些相似之处,但也有许多不同之处.下面我们详细介绍数组和切片的特点.用法以及它们之间的区别. 数组 数组是 ...

  6. vulnhub - NYX: 1

    vulnhub - NYX: 1 描述 这是一个简单的盒子,非常基本的东西. 它是基于vmware的,我不知道它是否可以在VB上运行,如果你愿意的话可以测试一下. /home/$user/user.t ...

  7. Ez Forensics详解

    Ez Forensics详解 题目要求: 数据库版本 + 字符集格式 + 最长列名 示例:NSSCTF 步骤: 解压压缩包得到forensics.vmdk,.vmdk是虚拟机磁盘文件的元数据文件 可以 ...

  8. 屏幕分辨率基础概念PX,PT,DP,DPR,DPI说明

    屏幕分辨率基础概念说明 缩写 全称 说明 PX Device Pixels 设备像素,指设备的物理像素 PX CSS Pixels CSS像素,指CSS样式代码中使用的逻辑像素 DOT Dot 点,屏 ...

  9. Vue3 之 reactive、ref、toRef、toRefs 使用与区别,源码分析详细注释

    目录 reactive.ref.toRef.toRefs 使用与区别 reactive ref 作用及用法 toRef 作用及用法 toRefs 作用及用法 ref,toRef,toRefs 源码实现 ...

  10. oeasy教您玩转vim - 24 - 自定颜色

    自定颜色 回忆上节课内容 这次我们研究了配色方案 找到了 colors 的位置 下载并应用了颜色方案 制作了自己的配色方案 下面我想修改配色方案的颜色 是否能成功??? 首先得有自己的颜色方案 #找到 ...