「JOISC 2014 Day2」水壶 题解

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

题意

给定一个 HW 列的方格,其中每个方格可能是空地或者障碍。

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

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

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

数据范围:1H,W2000,2P2105,1Q2105

图/树的建立

将建筑物视为点,按照水的消耗量作为边权来建边,那么这题本质上就是多次求任意两点之间的路径,且路径上的最大边权最小(货车运输狂喜:建立最小生成树后直接倍增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(logn) 的处理单次询问了。

//完整代码 #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(nn) 级别,老实说应该不好卡。

Kruskal重构树

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

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

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

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

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

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

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

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

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


__EOF__

本文作者cyhforlight
本文链接https://www.cnblogs.com/cyhforlight/p/16445132.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   cyhforlight  阅读(234)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示