[NOIP2024] 树上查询
题意
给定一棵 \(n\) 个点,\(m\) 条边的树,其中根节点为 \(1\)。定义 \(\mathrm{LCA*}(l,r)\) 为编号 \(l\sim r\) 所有点的最近公共祖先。接下来有 \(q\) 次询问:
- \(\texttt{l r k}\):查询 \(\max_{l\le l'\le r'\le r\land r'-l'+1\ge k} \text{dep}_{\mathrm{LCA*}(l',r')}\)
数据范围:\(1\le n,m\le 5\times 10^5\)。
解析
关键结论:\(\mathrm{dep}(\text{LCA*}(l,r))=\max_{i=l}^{r-1} \text{LCA*}(i,i+1)\)。
该结论可将树上问题转化为区间问题,即令 \(a_i=\text{LCA*}(i,i+1)\)。则对于每次查询,要求
这是 CF484E 的原题,做到 \(O(q\log^2 n)\) 是简单的。考虑二分答案 \(x\),将序列中小于等于 \(x\) 的数标为 \(1\),则只需要求 \(l\sim r\) 的最长连续 \(1\) 段的长度,并判断其是否大于等于 \(k\)。最长连续 \(1\) 段的长度使用主席树维护即可。
主席树常数大 \(O(q\log^2 n)\) 难以通过。接下来,讨论如何做到 \(O(q\log n)\)。考虑每个点作为最小值的区间记作 \((L_i,R_i)\),可通过单调栈求解。那么,当查询 \(l,r\) 时,只需要求与 \([l,r]\) 相交的长度大于等于 \(k\) 的区间中,权值最大的即可。列出贡献答案的区间的条件:
第一种情况,只需要对 \(R_i\) 扫描线维护;第二种情况,只需要对 \(R_i-L_i+1\) 扫描线维护,注意不能对 \(R_i\) 因为信息不可查分!
标程
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const int maxn = 500005;
int n, q;
std::vector<int> g[maxn];
int a[maxn], stk[maxn], top;
int fa[maxn][20], dep[maxn], ans[maxn];
array<int, 4> qry[maxn];
std::vector<array<int, 4>> kq[maxn], rq[maxn];
array<int, 3> seg[maxn];
std::vector<array<int, 3>> kf[maxn], rf[maxn];
void dfs(int u) {
for (int i = 1; i < 20; i ++)
fa[u][i] = fa[fa[u][i - 1]][i - 1];
for (auto v : g[u]) {
if (v == fa[u][0]) continue;
fa[v][0] = u, dep[v] = dep[u] + 1;
dfs(v);
}
}
int lca(int x, int y) {
if (dep[x] < dep[y]) swap(x, y);
for (int i = 19; i >= 0; i --)
if (dep[fa[x][i]] >= dep[y])
x = fa[x][i];
if (x == y) return x;
for (int i = 19; i >= 0; i --)
if (fa[x][i] != fa[y][i]) {
x = fa[x][i];
y = fa[y][i];
}
return fa[x][0];
}
struct SGT {
int mx[maxn * 4], m;
SGT() {
m = 1 << __lg(n) + 1;
memset(mx, 0, sizeof mx);
}
void set(int x, int d) {
x --, mx[x + m] = max(d, mx[x + m]);
for (int i = x + m >> 1; i; i /= 2)
mx[i] = max(mx[i * 2 + 1], mx[i * 2]);
}
int query(int l, int r) {
int L = l + m - 1, R = r + m - 1, res = 0;
for (; L <= R; L /= 2, R /= 2) {
if (L & 1) res = max(res, mx[L ++]);
if (~R & 1) res = max(res, mx[R --]);
}
return res;
}
};
int main() {
cin.tie(0);
cout.tie(0);
ios::sync_with_stdio(0);
cin >> n;
for (int i = 1; i < n; i ++) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
dep[1] = 1, dfs(1);
SGT one;
for (int i = 1; i <= n; i ++) one.set(i, dep[i]);
for (int i = 1; i < n; i ++)
seg[i][2] = a[i] = dep[lca(i, i + 1)];
for (int j : {0, 1}) {
top = 0;
for (int i = 1; i < n; i ++) {
while (top && a[stk[top]] >= a[i]) top --;
int t = j ? n - i : i;
if (top) seg[t][j] = !j ? stk[top] + 1 : n - stk[top] - 1;
else seg[t][j] = j ? n - 1 : 1;
stk[ ++ top] = i;
}
reverse(a + 1, a + n);
}
for (int i = 1; i < n; i ++) {
kf[seg[i][1] - seg[i][0] + 1].push_back(seg[i]);
rf[seg[i][1]].push_back(seg[i]);
}
cin >> q;
for (int i = 1; i <= q; i ++) {
for (int j : {0, 1, 2}) cin >> qry[i][j];
if (qry[i][2] == 1) {
ans[i] = one.query(qry[i][0], qry[i][1]);
continue;
}
qry[i][1] --, qry[i][2] --, qry[i][3] = i;
kq[qry[i][2]].push_back(qry[i]);
rq[qry[i][1]].push_back(qry[i]);
}
SGT sgt_k, sgt_r;
for (int i = n - 1; i >= 1; i --) {
for (auto [x, y, v] : rf[i]) sgt_r.set(x, v);
for (auto [l, r, k, j] : rq[i])
ans[j] = sgt_r.query(1, r - k + 1);
}
for (int i = n - 1; i >= 1; i --) {
for (auto [x, y, v] : kf[i])
sgt_k.set(y, v);
for (auto [l, r, k, j] : kq[i])
ans[j] = max(ans[j], sgt_k.query(l + k - 1, r));
}
for (int i = 1; i <= q; i ++)
cout << ans[i] << '\n';
return 0;
}
后记
考场上,只想到了可以通过 \(\tt dfn\) 序的方式,求解 \(\mathrm{LCA*}(l,r)\),但是很没有前途。扫描线的关键在于动态维护信息,在过程中查询!

浙公网安备 33010602011771号