ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

「JOISC 2014 Day2」水壶 题解

2022-07-05 09:00:47  阅读:151  来源: 互联网

标签:val JOISC int 题解 Day2 MH dep fa res


题目链接:LibreOJ #2876. 「JOISC 2014 Day2」水壶

题意

给定一个 \(H\) 行 \(W\) 列的方格,其中每个方格可能是空地或者障碍。

方格图中存在着 \(P\) 个建筑物,第 \(i\) 个建筑物的坐标是 \((A_i,B_i)\)(保证建筑物的位置一定是在空地上)。

现在,JOI君需要在各个建筑物间往返,但是太阳很大,所以需要带一个水壶,每经过一片空地就需要消耗一升水。我们可以在建筑物内把水壶补满。

现在有 \(n\) 次询问,目标是从建筑物 \(s\) 到达 \(t\),问至少需要多大的水壶?

数据范围:\(1\leq H,W\leq 2000,2\leq P \leq 2*10^5,1\leq Q \leq 2*10^5\)

图/树的建立

将建筑物视为点,按照水的消耗量作为边权来建边,那么这题本质上就是多次求任意两点之间的路径,且路径上的最大边权最小(货车运输狂喜:建立最小生成树后直接倍增LCA或者其他算法都行)。

问题在于,这题的点规模过大,使得朴素的建边法变得不再可行,必须令谋他路。

搜索中,有一个被称为 双向宽搜 的优化方式:从起点和终点分别开始搜索,在中间汇聚,这种方式一定程度上能够优化复杂度。在这题中,我们也采取类似方式:从 \(P\) 个点开始搜,经过一个点(空地)时候就给他打上前继所属点的标记(每个空地点都标上距离其最近的建筑物的距离)。

在方格图中,若存在两个方格,隶属于不同的建筑物,不妨分别标记为 \((A,i),(B,j)\),说明 \(A,B\) 间存在着一条权值 \(i+j\) 的无向边。

这种方式没有列出所有存在的边,但是保证了这些边的边权都是最小的那一批,且必然联通,因此不影响最小生成树的生成。

const int MH = 2010, N = 200010;
int H, W, P, Q;
char a[MH][MH];
int A[N], B[N];
struct Edge { int x, y, val; };
namespace BFS_Krurkal {
    // BFS
    //边最大能到4*10^6,注意了
    vector<Edge> edge[4000010];
    struct Node { int belong, dis; } node[MH][MH];
    queue<int> q;
    const int dx[4] = {1, 0, -1, 0}, dy[4] = {0, 1, 0, -1};
    inline int can(int x, int y) {
        return x >= 1 && x <= H && y >= 1 && y <= W && a[x][y] == '.';
    }
    inline int encode(int x, int y) { return (x - 1) * W + y; }
    inline void decode(int v, int &x, int &y) {
        x = (v - 1) / W + 1, y = (v - 1) % W + 1;
    }
    void BFS() {
        for (int i = 1; i <= P; ++i) {
            node[A[i]][B[i]] = (Node){i, 0};
            q.push(encode(A[i], B[i]));
        }
        while (!q.empty()) {
            int now = q.front(), x, y; q.pop();
            decode(now, x, y);
            Node &F = node[x][y];
            for (int i = 0; i < 4; ++i) {
                int tx = x + dx[i], ty = y + dy[i];
                if (!can(tx, ty)) continue;
                Node &T = node[tx][ty];
                if (T.belong) {
                    if (F.belong != T.belong) {
                        int dis = F.dis + T.dis;
                        edge[dis].push_back((Edge){F.belong, T.belong, dis});
                    }
                }
                else {
                    T.belong = F.belong, T.dis = F.dis + 1;
                    q.push(encode(tx, ty));
                }
            }
        }
    }
    // UnionSet
    int fa[N];
    void init() {
        for (int i = 1; i <= P; ++i)
            fa[i] = i;
    }
    int find(int x) {
        if (x != fa[x]) fa[x] = find(fa[x]);
        return fa[x];
    }
    // Kruskal
    void Kruskal(vector<Edge> &vec) {
        init();
        int tot = P;
        for (int val = 0; val < 4000010; ++val) {
            for (Edge e : edge[val]) {
                int x = e.x, y = e.y;
                x = find(x), y = find(y);
                if (x != y) {
                    fa[x] = y;
                    vec.push_back(e);
                    if (--tot == 1) break;
                }
            }
            if (tot == 1) break;
        }
    }
}

询问的处理

建立好了树后,接下来就是要处理多次询问:每次询问给定树上的两个点,求出路径上边权的最大值。

倍增LCA

边权最大值不具备加减性质,但是符合结合律,利用倍增的方式,在求 LCA 的过程中顺带维护一下,就可以 \(O(\log n)\) 的处理单次询问了。

//完整代码
#include <bits/stdc++.h>
using namespace std;
const int MH = 2010, N = 200010;
int H, W, P, Q;
char a[MH][MH];
int A[N], B[N];
struct Edge { int x, y, val; };
namespace BFS_Krurkal {
    //照搬上面的代码,把最小边都存进了vec里面
}
namespace LCA {
    vector<Edge> tree[N];
    int dep[N], lg[N], fa[N][20], mv[N][20];
    void dfs(int x, int f, int val) {
        dep[x] = dep[f] + 1;
        fa[x][0] = f, mv[x][0] = val;
        for (int i = 1; (1 << i) <= dep[x]; ++i) {
            fa[x][i] = fa[fa[x][i - 1]][i - 1];
            mv[x][i] = max(mv[x][i - 1], mv[fa[x][i - 1]][i - 1]);
        }
        for (Edge e : tree[x])
            if (e.y != f) dfs(e.y, x, e.val);
    }
    void build(vector<Edge> &vec) {
        for (Edge e : vec) {
            int x = e.x, y = e.y, val = e.val;
            tree[x].push_back((Edge){x, y, val});
            tree[y].push_back((Edge){y, x, val});
        }
        lg[1] = 0;
        for (int i = 2; i < N; ++i) lg[i] = lg[i / 2] + 1;
        for (int i = 1; i <= P; ++i)
            if (!dep[i]) dfs(i, 0, 0);
    }
    int LCA(int x, int y) {
        int res = 0;
        if (dep[x] < dep[y]) swap(x, y);
        while (dep[x] > dep[y]) {
            res = max(res, mv[x][lg[dep[x] - dep[y]]]);
            x = fa[x][lg[dep[x] - dep[y]]];
        }
        if (x == y) return res;
        for (int k = lg[dep[x]]; k >= 0; k--)
            if (fa[x][k] != fa[y][k]) {
                res = max(res, max(mv[x][k], mv[y][k]));
                x = fa[x][k], y = fa[y][k];
            }
        res = max(res, max(mv[x][0], mv[y][0]));
        return res;
    }
}
vector<Edge> vec;
int main() {
    // read
    scanf("%d%d%d%d", &H, &W, &P, &Q);
    for (int i = 1; i <= H; ++i)
        scanf("%s", a[i] + 1);
    for (int i = 1; i <= P; ++i)
        scanf("%d%d", &A[i], &B[i]);
    // build
    BFS_Krurkal::BFS();
    BFS_Krurkal::Kruskal(vec);
    // init
    LCA::build(vec);
    // query
    while (Q--) {
        int x, y;
        scanf("%d%d", &x, &y);
        if (BFS_Krurkal::find(x) != BFS_Krurkal::find(y)) puts("-1");
        else printf("%d\n", LCA::LCA(x, y));
    }
    return 0;
}

树上莫队

树上莫队,复杂度 \(O(n\sqrt{n})\) 级别,老实说应该不好卡。

Kruskal重构树

Kruskal重构树类似于最小生成树算法,不过构建流程如下:

  1. 先按照流程,得到 Kruskal 的所有边,按照边大小排序(从小到大)
  2. 开始构建这个重构树,从小到大枚举边,记 \(x,y\) 两点所在连通块的父亲根节点为 \(rx,ry\),那么新建一个权值为 \(w\) 的节点,把 \(rx,ry\) 分别接在该节点的左右儿子上面,直到所有边建立结束,最后构成一个 \(2n-1\) 的二叉树结构。

显然,这个二叉树结构是一个二叉堆(因为边是从小到大排序的,所以抛开原节点不谈,新节点都满足父节点权值大于等于子节点权值的性质)。

在这个二叉树上,我们可以很轻松的完成两个如下性质的任务:

  1. 求出一张无向图中,两点 \(x,y\) 之间的路径,要求这条路径上边权的最大值最小

    (货车运输狂喜)只要保持连通性即可,所以直接贪心得到最小生成树,然后求这条唯一路径上边权最大值即可。

    这个写起来有点小烦,但是用 Kruskal 重构树写起来就很方便:\(x,y\) 的LCA 所指向的节点的权值极为这个最大值。

  2. 求出无向图中,点 \(x\) 在经过不超过 \(w\) 权值的边的情况下,能达到的点的数量

    向上找到最高点,然后所在子树就是所有能达到的点的集合

标签:val,JOISC,int,题解,Day2,MH,dep,fa,res
来源: https://www.cnblogs.com/cyhforlight/p/16445132.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有