前言

题目链接:洛谷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. 如何解决Win10删除文件慢的办法

    问题:最近使用KMS激活了一些工具,今天删除不需要的文件时发现删除文件很慢很慢,删除一个几百k的文件都很慢. 解决办法通过控制面板→管理工具→服务→找到该进程并设为禁用就OK了.

  2. 安卓app 地铁最短路径查询 完成

    我通过三个函数 完成了这个功能 首先  创建哈希表 根据起始站名 终点站名 然后 根据哈希表 建立起 邻接表' 最后 根据迪杰斯特拉算法 完成这个功能 /** * function:起终查询 */ / ...

  3. JAVA-poi导出excel到http响应流

    导出结果为excel是相对常见的业务需求,大部分情况下只需要导出简单的格式即可,所以有许多可以采用的方案.有些方案还是很容易实现的. 一.可用的解决方案 目前可以有几类解决方案: 字处理企业提供的解决 ...

  4. gcc系列工具 介绍

    编译器相关知识学习 GNU GCC简介 GNU GCC是一套面向嵌入式领域的交叉编译工具,支持多种编程语言.多种优化选项并且能够支持分步编译.支持多种反汇编方式.支持多种调试信息格式,目前支持X86. ...

  5. 在WPF中使用WriteableBitmap对接工业相机及常用操作

    写作背景 写这篇文章主要是因为工业相机(海康.大恒等)提供的.NET开发文档和示例程序都是用WinForm项目来说明举例的,而在WPF项目中对图像的使用和处理与在WinForm项目中有很大不同.在Wi ...

  6. 2-SET详解

    前置知识 SET问题的标准定义:在计算机科学中,布尔可满足性问题(有时称为命题可满足性问题,缩写为SATISFIABILITY或SAT)是确定是否存在满足给定布尔公式的解释的问题.(全是废话) 说人话 ...

  7. 『vulnhub系列』Dripping-Blues-1

    『vulnhub系列』Dripping-Blues-1 下载地址: https://www.vulnhub.com/entry/dripping-blues-1,744/ 信息搜集: 使用nmap进行 ...

  8. 3562-Qt工程编译说明、GPU核心使用说明

  9. 写sql语句思路--28道关于教师、学生、成绩表的练习题---个人思路

    针对学生教师的28道练习题的思路 链接:https://pan.baidu.com/s/1TgqFAe7i0PAkZOm47-Jd0A 提取码:vvi6 部分截图如下: -- sql 28道练习题答案 ...

  10. 韦东山freeRTOS系列教程之【第五章】队列(queue)

    目录 系列教程总目录 概述 5.1 队列的特性 5.1.1 常规操作 5.1.2 传输数据的两种方法 5.1.3 队列的阻塞访问 5.2 队列函数 5.2.1 创建 5.2.2 复位 5.2.3 删除 ...