Kruskal重构树
先来看一道题:Peaks
下面我们考虑一个在线做法。
因为图上某个点能到达的部分点中第 \(k\) 大是不好直接维护的,但树和序列上第 \(k\) 大是很好维护的,因此我们可以将这个问题转化为一个树上或序列上的问题。
可以发现后者是难以转化的,因为序列就不能很好的体现联通性这一性质。
那么我们来考虑一下前者,首先我们不能将原问题转化成链上第 \(k\) 大的问题,因为链上第 \(k\) 大本质上与序列第 \(k\) 大相同。
那么另一个方向就是将问题转化为子树中第 \(k\) 大的问题。
首先我们要保证的是这整颗子树都是 \(u\) 所能到达的,并且你会发现由于每次查询最大不能经过的边权是会变化的,那么我们的构造方式必须对所有的查询具有普适性。
换句话说,对于任意一组查询 \(u \ x \ k\) 在我们构造的树上需要能找到一颗子树满足子树内所有边权不大于 \(x\) 且都能被 \(u\) 访问到。
那么你会发现这棵树靠上的边权必然不小于下方的边权,否则是不满足普适性的。
这就意味着我们要将边按照从小到大依次从叶子节点插入这棵树。
因为边权是很不方便统计的,因此我们将边权写成点权的形式,具体的加入一条边 \((u, v)\) 我们新建一个点 \(t\) 连接 \((u, t), (v, t)\) 将 \(t\) 的权值设置为 \((u, v)\) 的边权。
那么我们只剩下最后一个问题了,怎么让一棵子树内的点 \(u\) 都能到达并且满足查询的普适性呢?
不难发现每次连接一条边 \((u, v)\) 将 \(u, v\) 分别所在连通块连接即可。
于此同时为了保证父亲的边权不小于儿子的边权,因为当前插入的边比之前所有边都大,因此你需要将两个连通块顶端两点连接,特殊的如果这两个点已经联通,向上连一条即可。
回顾一下上面的流程,将边按照权值从小到大排序,每次加入一条边 \((u, v)\) 将 \(u, v\) 所在连通块顶端和新建的一个虚点相连,将虚点点权设置为边权,那么每次查询 \(u \ x \ k\) 就找到 \(u\) 最上方点权不大于 \(x\) 的祖先 \(u'\) 查询 \(u'\) 子树内点权第 \(k\) 大的点即可。
不难发现这个找第一个的过程可以使用树上倍增实现,查询子树内第 \(k\) 大的点既可以线段树合并也可以使用主席树实现。
#include <bits/stdc++.h>
using namespace std;
#define ls t[p].l
#define rs t[p].r
#define mid (l + r >> 1)
#define rep(i, l, r) for (int i = l; i <= r; ++i)
#define dep(i, l, r) for (int i = r; i >= l; --i)
#define Next(i, u) for (int i = h[u]; i; i = e[i].next)
const int N = 600000 + 5;
const int M = 100000 + 5;
struct tree { int sum, l, r;} t[M * 20];
struct edge { int v, next;} e[N << 1];
struct node { int u, v, w;} r[N];
int n, m, q, k, u, v, x, y, F, tot, cnt, num, res, tmp;
int a[M], d[M], h[N], rt[N], sz[N], fa[N], val[N], dfn[N], f[N][20];
int read() {
char c; x = 0, F = 1;
c = getchar();
while (c > '9' || c < '0') { if(c == '-') F = -1; c = getchar();}
while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
return x * F;
}
bool cmp(node a, node b) { return a.w < b.w;}
int find(int x) { return fa[x] == x ? fa[x] : fa[x] = find(fa[x]);}
void add(int u, int v) {
e[++tot].v = v, e[tot].next = h[u], h[u] = tot;
e[++tot].v = u, e[tot].next = h[v], h[v] = tot;
}
void dfs(int u, int Fa) {
dfn[u] = ++cnt, fa[cnt] = u, f[u][0] = Fa, sz[u] = 1;
rep(i, 1, 19) f[u][i] = f[f[u][i - 1]][i - 1];
Next(i, u) if(e[i].v != Fa) dfs(e[i].v, u), sz[u] += sz[e[i].v];
}
void build(int &p, int l, int r) {
p = ++num; if(l == r) return;
build(ls, l, mid), build(rs, mid + 1, r);
}
void update(int &p, int k, int l, int r, int x, int y) {
p = ++num, t[p] = t[k], ++t[p].sum;
if(l == r) return;
if(mid >= x) update(ls, t[k].l, l, mid, x, y);
if(mid < y) update(rs, t[k].r, mid + 1, r, x, y);
}
int query(int p, int k, int l, int r, int x) {
if(t[p].sum - t[k].sum < x) return -1;
if(l == r) return d[l];
tmp = t[rs].sum - t[t[k].r].sum;
if(tmp >= x) return query(rs, t[k].r, mid + 1, r, x);
else return query(ls, t[k].l, l, mid, x - tmp);
}
int solve(int u, int x, int k) {
dep(i, 0, 19) if(val[f[u][i]] <= x && f[u][i]) u = f[u][i];
return query(rt[dfn[u] + sz[u] - 1], rt[dfn[u] - 1], 1, res, k);
}
int main() {
n = read(), m = read(), q = read();
rep(i, 1, n) a[i] = read(), d[++res] = a[i];
rep(i, 1, m) r[i].u = read(), r[i].v = read(), r[i].w = read();
sort(d + 1, d + res + 1), sort(r + 1, r + m + 1, cmp);
res = unique(d + 1, d + res + 1) - d - 1;
rep(i, 1, n) a[i] = lower_bound(d + 1, d + res + 1, a[i]) - d;
rep(i, 1, n + m) fa[i] = i;
rep(i, 1, m) {
u = r[i].u, v = r[i].v, val[i + n] = r[i].w;
x = find(u), y = find(v);
if(x == y) add(i + n, x), fa[x] = i + n;
else add(i + n, x), add(i + n, y), fa[x] = fa[y] = i + n;
}
dep(i, 1, n + m) if(!dfn[i]) dfs(i, 0);
build(rt[0], 1, res);
rep(i, 1, n + m) {
if(fa[i] <= n) update(rt[i], rt[i - 1], 1, res, a[fa[i]], a[fa[i]]);
else rt[i] = rt[i - 1];
}
while (q--) u = read(), v = read(), k = read(), printf("%d\n", solve(u, v, k));
return 0;
}
从这个问题我们可以发现一类处理形如:从某个点 \(u\) 开始经过满足某个条件的边能走到的点中满足一些限制的点。这样一类问题。
而解决方法往往是将边按照某种顺序排序后,按照上面这题的建树方式将问题转化为查询子树内满足某个条件的点的问题。
这种算法就是大名鼎鼎的 \(\rm Kruskal\) 重构树。