Loading

[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)\)。则对于每次查询,要求

\[\max_{l\le l'\le r'\le r\land r'-l'+1\ge k}\left( \min_{j=l'}^{r'} \left( a_j\right) \right) \]

这是 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\ge r\land L_i\le r-k+1\quad \lor\quad l+k-1\le R_i\le r\land R_i-L_i+1\ge 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)\),但是很没有前途。扫描线的关键在于动态维护信息,在过程中查询!

posted @ 2025-03-21 21:05  Pigsyy  阅读(111)  评论(4)    收藏  举报