「JOISC 2014 Day2」水壶 题解
作者:互联网
题目链接: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重构树类似于最小生成树算法,不过构建流程如下:
- 先按照流程,得到 Kruskal 的所有边,按照边大小排序(从小到大)
- 开始构建这个重构树,从小到大枚举边,记 \(x,y\) 两点所在连通块的父亲根节点为 \(rx,ry\),那么新建一个权值为 \(w\) 的节点,把 \(rx,ry\) 分别接在该节点的左右儿子上面,直到所有边建立结束,最后构成一个 \(2n-1\) 的二叉树结构。
显然,这个二叉树结构是一个二叉堆(因为边是从小到大排序的,所以抛开原节点不谈,新节点都满足父节点权值大于等于子节点权值的性质)。
在这个二叉树上,我们可以很轻松的完成两个如下性质的任务:
-
求出一张无向图中,两点 \(x,y\) 之间的路径,要求这条路径上边权的最大值最小
(货车运输狂喜)只要保持连通性即可,所以直接贪心得到最小生成树,然后求这条唯一路径上边权最大值即可。
这个写起来有点小烦,但是用 Kruskal 重构树写起来就很方便:\(x,y\) 的LCA 所指向的节点的权值极为这个最大值。
-
求出无向图中,点 \(x\) 在经过不超过 \(w\) 权值的边的情况下,能达到的点的数量
向上找到最高点,然后所在子树就是所有能达到的点的集合
标签:val,JOISC,int,题解,Day2,MH,dep,fa,res 来源: https://www.cnblogs.com/cyhforlight/p/16445132.html