SP208 STORE - Store-keeper 题解

前置知识

  1. 广搜
  2. 树上倍增
  3. 一些连通性问题的概念理论的理解即应用。

思路

首先明确一下:双层广搜的时间复杂度是假的。

这道题的暴力不言而喻,即双层广搜。凡是普及组学得比较扎实的人基本能想到。 即外层是一个广搜框架,即队列里的元素记录箱子的位置 $(x,y)$ 以及人和箱子的位置关系。而通过人和箱子之间的位置关系我们可以得知箱子下一步会被推到 $(a,b)$ 这个位置。至于拓展出地图上新的没遍历到过的位置,我们只要枚举箱子的新位置 $a,b$ 和人的位置关系即可。而为了判断新状态的合法性,我们设中 $(fx,fy)$ 为新状态中人的位置,那么我们只要判断新状态的位置是否在棋盘内、新状态是否在历史里已经有过了以及 $(x,y)$ 是否能不通过 $(a,b)$ 走到 $(fx,fy)$ 这个位置,如果以上条件都满足,新状态合法,压入队列。

上述算法外层一个广搜,而内层为判断 $(x,y)$ 是否能不通过 $(a,b)$ 走到 $(fx,fy)$ 这个位置,最坏时间复杂度 $O(n^2m^2)$ 级别,本来稳稳地时间超限,但是因为数据比较水,且可能很多情况跑不满,就被水过了。

我们思考复杂度的瓶颈在于哪里——内层的广搜。如果优化他?按照数据范围来说似乎只有那种对数阶的才能过。所以我们先给每个节点标号,相邻的合法节点间连边,建立出一颗深度优先搜索树,那么我们要判断 $x$ 是否能不经过 $z$ 到达 $y$ 节点,只需要分类讨论:

  1. $x,y$ 都在 $z$ 的子树内。

    设 $z$ 的所有儿子中是满足是 $x$ 的祖先的那个节点编号是是 $x'$ 节点,而 $z$ 的所有儿子中是满足是 $y$ 的祖先的那个节点编号是 $y'$ 节点。

    其中 $x',y'$ 这两个节点可以通过倍增得到。

    • 如果存在一条 $x'$ 连出去的非树边使得指向节点的编号小于 $z$ 节点,且同时存在一条 $y'$ 连出去的非树边使得指向节点的编号小于 $z$ 节点,那么可以。

    • 否则 $z$ 是割点,继续判断。

      • 如果 $x,y$ 处于 $z$ 的同一颗子树内,也就是这时 $x'=y'$ 则说明可以。
      • 那么不处于 $z$ 的同一颗子树内,则不可以。
  2. 只有一个节点在 $z$ 的子树内。

    我们假设 $x$ 为在 $z$ 子树里的那个节点,而 $x'$ 定义同上。

    • 如果存在一条 $x'$ 连出去的非树边使得指向节点的编号小于 $z$ 节点,那么可以。
    • 否则证明 $z$ 是割点,不可以。
  3. 都不在 $z$ 的子树内。

    那必定可以。

代码

#include <bits/stdc++.h>
#define L(i, a, b) for (int i = a; i <= b; i++)
#define R(i, a, b) for (int i = a; i >= b; i--)
#define E(i, u) for (int i = h[u]; ~i; i = e[i].nxt)
using namespace std;
const int N = 110, M = 10010;
struct Pos {int x, y, o;};
struct Edge {int v, nxt;} e[M << 2];
int n, m, tt, tot, tote, h[M], de[M], dfn[M], low[M], l[M], r[M], lg[M];//注意这里的low数组和tarjan算法中的low数组定义稍有出入
int id[N][N], vis[N][N], fa[M][15], d[N][N][4], f[4][2] = { 0, 1, 1, 0, 0, -1, -1, 0 };
char mp[N][N]; queue<Pos> q;
void Adde(int u, int v) { e[++tote] = { v, h[u] }, h[u] = tote; }
void Dfs(int x, int y) {//获得一开始人到箱子旁边的步数
    vis[x][y] = 1;
    L(i, 0, 3) {
        int fx = x + f[i][0], fy = y + f[i][1];
        if (id[fx][fy] && mp[fx][fy] != 'S') {
            if (mp[fx][fy] == 'P')
                d[fx][fy][i] = 1, q.push({ fx, fy, i });
            else if(!vis[fx][fy])
                Dfs(fx, fy);
        }
    }
}
void Tarjan(int x) {//建立dfs数
    dfn[x] = low[x] = l[x] = ++tot;
    de[x] = de[fa[x][0]] + 1;
    L(i, 1, lg[de[x]]){
        fa[x][i] = fa[fa[x][i - 1]][i - 1];
        if(!fa[x][i]) break;
    }
    E(i, x) {
        int v = e[i].v;
        if (v == fa[x][0]) continue;
        if (!dfn[v])
            fa[v][0] = x, Tarjan(v);
        low[x] = min(low[x], low[v]);
    }
    r[x] = tot;
}
bool ck(int x, int y) { return l[y] <= dfn[x] && dfn[x] <= r[y]; }//判断x是否在y的子树中
int up(int x, int y) {//把x向上跳y步,用于上述思路中的x跳到x'
    for(int i = lg[y]; i >= 0; i--){
        if(y & (1 << i)){
            y -= (1 << i);
            x = fa[x][i];
        }
    }
    return x;
}
bool check(int x, int y, int z) {//判断x能否不经过z到达y
    if (!ck(x, z) && !ck(y, z))
        return 1;
    if (ck(x, z) + ck(y, z) == 1) {
        if (ck(y, z)) swap(x, y);
        x = up(x, de[x] - de[z] - 1);
        return (low[x] < l[z]);
    }
    x = up(x, de[x] - de[z] - 1);
    y = up(y, de[y] - de[z] - 1);
    if (low[x] < dfn[z] && low[y] < dfn[z])
        return 1;
    return (x == y);
}
void Init(){//多测不清空,十年OI一场空
    tt = tot = tote = 0;
    while(!q.empty()) q.pop();
    memset(d, 0, sizeof(d));
    memset(h, -1, sizeof(h));
    memset(fa, 0, sizeof(fa));
    memset(dfn, 0, sizeof(dfn));
    memset(vis, 0, sizeof(vis));
    memset(id, 0, sizeof(id));//我之前这里没清空,而id数组还有判越界的效用
}
void Solve() {
    Pos s; Init();
    scanf("%d%d", &n, &m);
    lg[0] = -1; L(i, 1, n * m) lg[i] = lg[i >> 1] + 1;
    L(i, 1, n) {
        scanf("%s", mp[i] + 1);
        L(j, 1, m) {
            id[i][j] = ++tt;
            if (mp[i][j] == 'M')
                s = { i, j };
        }
    }
    L(i, 1, n) L(j, 1, m) {
        if (mp[i][j] == 'S') continue;
        L(k, 0, 3) {
            int x = i + f[k][0], y = j + f[k][1];
            if (id[x][y] && mp[x][y] != 'S')
                Adde(id[i][j], id[x][y]);//相邻合法节点间建边
        }
    }
    Dfs(s.x, s.y), Tarjan(id[s.x][s.y]);
    while (!q.empty()) { //广搜框架
        Pos t = q.front(); q.pop();
        int x = t.x, y = t.y, o = t.o, a = x + f[o][0], b = y + f[o][1];//含义如思路中所述
        if (!id[a][b] || mp[a][b] == 'S' || d[a][b][o])//如果搜过了就不再继续
            continue;
        if (mp[a][b] == 'K'){
            printf("%d\n", d[x][y][o]);
            return; 
        }
        d[a][b][o] = d[x][y][o] + 1;
        q.push({ a, b, o });
        L(i, 0, 3) {
            int fx = a - f[i][0], fy = b - f[i][1];//获取人的位置,如思路中所述
            if (i == o || !id[fx][fy] || mp[fx][fy] == 'S')
                continue;
            if (check(id[x][y], id[fx][fy], id[a][b])) {
                d[a][b][i] = d[x][y][o] + 1;
                q.push({ a, b, i });
            }
        }
    }
    puts("NO");
}
int main(){
    int T; scanf("%d", &T);
    while(T--) Solve(); 
    return 0;
}
posted @ 2023-02-03 16:57  徐子洋  阅读(11)  评论(0)    收藏  举报  来源