update 2024/12/28

题目描述

给定一棵树,每次询问区间 \([l,r]\) 的

\[\max_{l \le l' \le r' \le r \land r' - l' + 1 \ge k}\text{dep}_ {\text{LCA*}(l', r')}
\]

引理证明

先来证两个区间 \(\text{LCA}\) 的引理:

对于 \(\text{LCA} \{ l, l + 1, \dots r\}\) 我们有 \(\text{LCA} \{ l, l + 1, \dots r\}\) 为 \([l, r]\) 中 \(\text{dfs}\) 序最小的点和最大的点的 \(\text{LCA}\) 。证明:

假设点 \(u\) 是区间 \([l, r]\) 中 \(\text{dfs}\) 序最小的点 \(i\) 和最大的点 \(j\) 的 \(\text{LCA}\) ,则 \(dfn_u \leq dfn_i \leq dfn_j \leq out_u\)。于是我们对于区间 \([l, r]\) 中任意一个点 \(v\) 有 \(dep_i \leq dep_v \leq dep_j\),则 \(dep_u \leq dep_v \leq out_u\) ,所以区间中所有点都在 \(u\) 的子树中。


此外我们有 \(\text{LCA} \{ l, l + 1, \dots , r \}\) 为所有 \(\text{LCA(i, i + 1)} (l \leq i < r)\) 中最靠近根节点的那个。证明:

记 \(c_i = dep_{\text{LCA}(i, i + 1)}\) ,则对于结点 \(u\) ,\(v\) 若 \(dep_u \leq dep_v\) 可以说点 \(u\) 比点 \(v\) 更靠近根节点。设点 \(u\) 是区间 \([l, r]\) 的 \(\text{LCA}\) ,则一定存在两个点 \(x\) ,\(y\) 来自 \(u\) 的不同子树,此时 \([x, y)\) 中一定存在一个 \(v\) 使得 \(v\) 和 \(v + 1\) 来自不同子树,即 \(c_v = dep_u\) ,此时 \(\text{LCA} (v, v + 1) = u\) 。

思路推导

首先,对于区间 \(\text{LCA}\) ,我们已知有两个求法了,在此题中,如果要找 \(\text{dfs}\) 序最小最大需要额外维护一次最大最小,是没有前途的,所以针对第二种方法进行优化。

将 \(i\) 和 \(i + 1\) 两点的 \(dep_{\text{LCA}}\) 作为 \(c_i\) ,问题就转化为对于每个区间求

\[\max_{l \le l' \le r' \le r \land r' - l' + 1 \ge k}{\max_{l' \leq i < r'}{c_i}}
\]

对于 \(k = 1\) 的情况额外用 \(\text{RMQ}\) 处理。

首先我们很容易想到的是对于区间长度大于 \(k\) 的一定不优于区间长度等于 \(k\) 的,这一点很好证明,于是式子就变成

\[\max_{l \le l' \le r' \le r \land r' - l' + 1 = k}{\max_{l' \leq i < r'}{c_i}}
\]

一个相对来说比较容易想到的方法是计算每个点作为 \(\text{LCA}\) 的贡献。对于任意一个 \(i\) ,我们定义 \(pre_i\) 表示在 \(i\) 左边第一个 \(j + 1\) 使得 \(c_j \leq c_i\) ,\(suf_i\) 表示在 \(i\) 右边第一个 \(j - 1\) 使得 \(c_j < c_i\)(这一定是左边取 \(\leq\) 右边取 \(<\) 或左边取 \(<\) 右边取 \(\leq\),原因后文再说),这可以通过单调栈求取。对于 \(i\) ,能产生贡献的区间为 \(l=[pre_i + 1 , i]\) ,\(r=[i , suf_i - 1]\) ,容易想到转化为二位数点问题,该贡献区间可转化为以下矩形:



对于查询,也就转换为了一条斜率为 \(1\) 的线段,如下图:



显然对于 \(3\) 个方向的扫描线我们是没法维护的(至少我不知道),所以我们要将其拆分。

对于查询的线段,我们把它分为两个情况:

  • 全部被包含于一个矩形
  • 跨越多个矩形

对于第 \(1\) 种情况,扫描线板子解决。

对于第 \(2\) 种情况,我们可以发现如果该线段跨越矩阵的话一定会与矩形的边界有交点,于是我们可以只保留矩形的 \(4\) 条边,再拆分为横向和竖向的 \(2\) 对边,加上查询的斜线,就转换为两遍分别有两个方向的扫描线,如下图:

横向边贡献 \(+\) 查询



竖向边贡献 \(+\) 查询



但我们看到对于查询线段是斜着的,是不能用扫描线查询的,但整张图中是只有两个方向的,所以可以想到通过旋转坐标轴的 \(\hat{i}\) 和 \(\hat{j}\) 来拉平查询线段使得所有线“横平竖直”。

对于第 \(2\) 张图拉直过后像这样:



对于第一张图留给读者思考(才不是因为我不想画了)

最后还有一个问题:扫描线取最大值的影响怎么消除,这就关系到前文提到关于区间边界的问题了,正常情况下所有贡献矩形是不会产生交集的(原因可以自己手推一下一对 \(i,j\) 使得 \(i \leq j\) ,\(c_i < c_j\) 和 \(c_j < c_i\)的两种情况),除非出现两个 \(c_i\) 相等,此时如前文所述改为类似左闭右开的方法便可令其也不产生交集同时不重不漏。此时维护扫描线即可区间覆盖,区间取 \(\max\) 了。

所以统共来说是 \(3\) 遍扫描线 \(+\) \(1\) 遍 \(\text{RMQ}\),还是比较恶心的(当然是我的思路和做法有些复杂了,其它还有些相对更简便的做法)。

解法概括

对于点 \(i\) 定义 \(c_i\) 为 \(dep_{\text{LCA}(i,i + 1)}\) ,转换为序列问题。

若 \(k \geq 2\) 则对于 \(c_i\) 产生的贡献矩形将其拆分为两对相互平行的线段,旋转坐标轴,让查询斜线变为横线或竖线,进行 \(2\) 遍扫描线,再与线段一端点值取 \(\max\) ,对于 \(k = 1\) 的情况跑一遍 \(\text{RMQ}\) ,最后统计答案

实现

注意一些关于下标、边界的实现细节。

我之前是将线段树的功能全用区修区查来实现的,同时扫描线无论特殊性全写在 solve 中,第一次过了,没注意 \(1.98s\) 的时间,后来再交就一直 \(\text{TLE}\) ,于是修改了一些不必要的懒标记和区修,哪怕删了注释的调试,代码还是多了三十几行,但能保证不 \(\text{TLE}\) 了。

对于我这个思路,理论时间复杂度为 \(O(N log N)\) ,但常数巨大,按最坏情况分析:扫描线 \(4\) 倍,线段树 \(4\) 倍,\(3\) 遍扫描线,总计 \(\times 48\) ,但实际只有大约一半的常数。

#include<bits/stdc++.h>
using namespace std;
const int N = 5e5 + 5;
int n, q;
vector<int>G[N];
struct line {
int x, y, k;
bool operator < (const line& o)const { return x == o.x ? k < o.k : x < o.x; }
}li[N << 2];
struct node {
int x, dy, ty, k;
bool operator < (const node& o)const { return x == o.x ? k < o.k : x < o.x; }
}li2[N << 2];
struct query {
int x, l, r, id;
bool operator < (const query& o)const { return x < o.x; }
}qry[N];
int ans[N];
struct SegmentTree {
#define ls (id << 1)
#define rs (id << 1 | 1)
#define mid (l + r >> 1)
struct Segment {
int mx;
}seg[N << 2];
inline void merge(int id) { seg[id].mx = max(seg[ls].mx, seg[rs].mx); }
inline void build(int id, int l, int r) {
seg[id].mx = 1;
if (l == r) return;
build(ls, l, mid);build(rs, mid + 1, r);
}
inline void pushdown(int id) {
if (seg[id].mx != -1) {
seg[ls].mx = seg[id].mx;
seg[rs].mx = seg[id].mx;
seg[id].mx = -1;
}
}
inline void change(int id, int l, int r, int x, int k) {
if (l == r) {
seg[id].mx = k;
return;
}
if (x <= mid) change(ls, l, mid, x, k);
else change(rs, mid + 1, r, x, k);
merge(id);
}
inline int query(int id, int l, int r, int L, int R) {
if (l >= L && R >= r) return seg[id].mx;
int ret = 1;
if (L <= mid) ret = max(ret, query(ls, l, mid, L, R));
if (R > mid) ret = max(ret, query(rs, mid + 1, r, L, R));
return ret;
}
inline void modify(int id, int l, int r, int L, int R, int k) {
if (l >= L && R >= r) {
seg[id].mx = k;
return;
}
pushdown(id);
if (L <= mid) modify(ls, l, mid, L, R, k);
if (R > mid) modify(rs, mid + 1, r, L, R, k);
}
inline int get(int id, int l, int r, int x) {
if (l == r) return seg[id].mx;
pushdown(id);
if (x <= mid) return get(ls, l, mid, x);
else return get(rs, mid + 1, r, x);
}
}SGT;
const int K = 21;
int up[K + 1][N], dep[N];
inline void dfs(int u, int fa) {
for (int i = 1;i < K;i++)
up[i][u] = up[i - 1][up[i - 1][u]];
for (auto v : G[u]) {
if (v == fa) continue;
up[0][v] = u;
dep[v] = dep[u] + 1;
dfs(v, u);
}
}
inline void lift(int& x, int k) {
for (int i = K - 1;i >= 0;i--)
if (k >> i & 1) x = up[i][x];
}
inline int lca(int a, int b) {
lift(a, dep[a] - min(dep[a], dep[b]));
lift(b, dep[b] - min(dep[a], dep[b]));
if (a == b) return a;
for (int i = K - 1;i >= 0;i--)
if (up[i][a] != up[i][b]) a = up[i][a], b = up[i][b];
return up[0][a];
}
int l0[N], r0[N], k0[N];
int pre[N], nxt[N], c[N];
int stk[N], top;
int qc, m;
inline void solve() {
sort(qry + 1, qry + qc + 1);
sort(li + 1, li + m + 1);
SGT.build(1, 1, n);
int j = 1;
for (int i = 1;i <= qc;i++) {
while (li[j].x <= qry[i].x && j <= m) {
SGT.change(1, 1, n, li[j].y, li[j].k);
j++;
}
ans[qry[i].id] = max(ans[qry[i].id], SGT.query(1, 1, n, qry[i].l, qry[i].r));
}
}
int ST[K + 1][N], LG[N];
inline void init() {
for (int i = 2;i <= n;i++) LG[i] = LG[i >> 1] + 1;
for (int i = 1;i <= n;i++) ST[0][i] = dep[i];
for (int i = 1;i < K;i++)
for (int j = 1;j + (1 << i) - 1 <= n;j++)
ST[i][j] = max(ST[i - 1][j], ST[i - 1][j + (1 << i - 1)]);
}
inline int RMQ(int l, int r) {
int i = LG[r - l + 1];
return max(ST[i][l], ST[i][r - (1 << i) + 1]);
}
int main() {
// freopen("query.in", "r", stdin);
// freopen("query.out", "w", stdout);
scanf("%d", &n);
for (int i = 1;i < n;i++) {
int u, v;scanf("%d%d", &u, &v);
G[u].push_back(v);G[v].push_back(u);
}
scanf("%d", &q);
for (int i = 1;i <= q;i++) scanf("%d%d%d", &l0[i], &r0[i], &k0[i]);
dep[1] = 1;
dfs(1, 0);
for (int i = 1;i < n;i++) c[i] = dep[lca(i, i + 1)];
// for (int i = 1;i < n;i++) printf("%d ", c[i]);
// puts("");
stk[++top] = 0;
for (int i = 1;i < n;i++) {
while (c[stk[top]] >= c[i] && top > 0) top--;
pre[i] = stk[top] + 1;
stk[++top] = i;
}
top = 0;
stk[++top] = n;
for (int i = n - 1;i >= 1;i--) {
while (c[stk[top]] > c[i] && top > 0) top--;
nxt[i] = stk[top] - 1;
stk[++top] = i;
}
// for (int i = 1;i < n;i++) printf("%d ", pre[i]);
// puts("");
// for (int i = 1;i < n;i++) printf("%d ", nxt[i]);
// puts("");
/*
matrix: (pre[i], nxt[i]) -------- (i, nxt[i]) (pre[i], i) ------------------(i, i) line:
(r0[i] - k0[i] + 1, r0[i] - 1)
/
/
/
/
/
/
/
/
/
(l0[i], l0[i] + k0[i] - 2)
*/
m = qc = 0;
for (int i = 1;i < n;i++) {
li[++m] = { pre[i] - nxt[i],nxt[i],c[i] };
li[++m] = { i + 1 - nxt[i],nxt[i], 1 };
li[++m] = { pre[i] - i,i,c[i] };
li[++m] = { i + 1 - i,i,1 };
}
for (int i = 1;i <= q;i++)
if (k0[i] > 1)
qry[++qc] = { l0[i] - (l0[i] + k0[i] - 2),l0[i] + k0[i] - 2,r0[i] - 1,i };
solve();
/*
matrix: (pre[i], nxt[i]) (i, nxt[i])
| |
| |
| |
| |
| |
| |
(pre[i], i) (i, i) line:
(r0[i] - k0[i] + 1, r0[i] - 1)
/
/
/
/
/
/
/
/
/
(l0[i], l0[i] + k0[i] - 2)
*/
m = qc = 0;
for (int i = 1;i < n;i++) {
li[++m] = { i - pre[i],pre[i],c[i] };
li[++m] = { nxt[i] + 1 - pre[i],pre[i],1 };
li[++m] = { i - i,i,c[i] };
li[++m] = { nxt[i] + 1 - i,i,1 };
}
for (int i = 1;i <= q;i++)
if (k0[i] > 1)
qry[++qc] = { l0[i] + k0[i] - 2 - l0[i],l0[i],r0[i] - k0[i] + 1,i };
solve();
/*
matrix: (pre[i], nxt[i]) -------- (i, nxt[i])
| |
| |
| |
| |
| |
| |
(pre[i], i) ------------------(i, i) line:
.
(l0[i], l0[i] + k0[i] - 2)
*/
m = qc = 0;
for (int i = 1;i < n;i++) {
li2[++m] = { pre[i],i,nxt[i],c[i] };
li2[++m] = { i + 1,i,nxt[i],1 };
}
for (int i = 1;i <= q;i++)
if (k0[i] > 1)
qry[++qc] = { l0[i],l0[i] + k0[i] - 2,l0[i] + k0[i] - 2,i };
// for (int i = 1;i <= m;i++) printf("%d %d %d %d\n", li[i].x, li[i].dy, li[i].ty, li[i].k);
sort(qry + 1, qry + qc + 1);
sort(li2 + 1, li2 + m + 1);
SGT.build(1, 1, n);
int j = 1;
for (int i = 1;i <= qc;i++) {
while (li2[j].x <= qry[i].x && j <= m) {
SGT.modify(1, 1, n, li2[j].dy, li2[j].ty, li2[j].k);
j++;
}
ans[qry[i].id] = max(ans[qry[i].id], SGT.get(1, 1, n, qry[i].l));
}
init();
for (int i = 1;i <= q;i++)
if (k0[i] == 1) ans[i] = max(ans[i], RMQ(l0[i], r0[i]));
for (int i = 1;i <= q;i++) printf("%d\n", ans[i]);
return 0;
}

「NOIP2024」 树上查询的更多相关文章

  1. 「HAOI2015」「LuoguP3178」树上操作(树链剖分

    题目描述 有一棵点数为 N 的树,以点 1 为根,且树点有边权.然后有 M 个操作,分为三种: 操作 1 :把某个节点 x 的点权增加 a . 操作 2 :把某个节点 x 为根的子树中所有点的点权都增 ...

  2. 「HAOI2015」树上操作(非树剖)

    题目链接(luogu) 看到标签::树链剖分,蒟蒻Sy开始发抖,不知所措,但其实,本题只需要一个恶心普通的操作就可以了!! 前提知识:欧拉序 首先我们知道dfs序,就是在dfs过程中,按访问顺序进行编 ...

  3. 「ZJOI2013」K大数查询

    「ZJOI2013」K大数查询 传送门 整体二分,修改的时候用线段树代替树状数组即可. 参考代码: #include <cstdio> #define rg register #defin ...

  4. 数据结构1 「在线段树中查询一个区间的复杂度为 $O(\log N)$」的证明

    线段树属于二叉树, 其核心特征就是支持区间加法,这样就可以把任意待查询的区间$[L, R]$分解到线段树的节点上去,再把这些节点的信息合并起来从而得到区间$[L,R]$的信息. 下面证明在线段树上查询 ...

  5. 「咕咕网校 - 基础省选」树上问题的进阶 by Drench

    一定要在noip之前把自己花钱买的Luogu网课梳理完!QAQ 树上前缀和: 对于有根树,在每个点记录 val (点权) 和 sum(到根的点权之和) 当然记录的值因题而异(但是既然叫树上前缀和当然就 ...

  6. LOJ #2116 Luogu P3241「HNOI2015」开店

    好久没写数据结构了 来补一发 果然写的时候思路极其混乱.... LOJ #2116 Luogu P3241 题意 $ Q$次询问,求树上点的颜色在$ [L,R]$中的所有点到询问点的距离 强制在线 询 ...

  7. LOJ #2359. 「NOIP2016」天天爱跑步(倍增+线段树合并)

    题意 LOJ #2359. 「NOIP2016」天天爱跑步 题解 考虑把一个玩家的路径 \((x, y)\) 拆成两条,一条是 \(x\) 到 \(lca\) ( \(x, y\) 最近公共祖先) 的 ...

  8. LOJ2135 「ZJOI2015」幻想乡战略游戏

    题意 题目描述 傲娇少女幽香正在玩一个非常有趣的战略类游戏,本来这个游戏的地图其实还不算太大,幽香还能管得过来,但是不知道为什么现在的网游厂商把游戏的地图越做越大,以至于幽香一眼根本看不过来,更别说和 ...

  9. 「NOI2018」你的名字

    「NOI2018」你的名字 题目描述 小A 被选为了\(ION2018\) 的出题人,他精心准备了一道质量十分高的题目,且已经 把除了题目命名以外的工作都做好了. 由于\(ION\) 已经举办了很多届 ...

  10. LOJ #2585. 「APIO2018」新家

    #2585. 「APIO2018」新家 https://loj.ac/problem/2585 分析: 线段树+二分. 首先看怎样数颜色,正常的时候,离线扫一遍右端点,每次只记录最右边的点,然后查询左 ...

随机推荐

  1. python之typing

    typing介绍 Python是一门动态语言,很多时候我们可能不清楚函数参数类型或者返回值类型,很有可能导致一些类型没有指定方法,在写完代码一段时间后回过头看代码,很可能忘记了自己写的函数需要传什么参 ...

  2. PythonDay6Advance

    PythonDay6Advance 模块.类与对象 模块 内置模块 time, random, os, json 第三方模块 requests, pandas, numpy,.... 自定义模块 xx ...

  3. PYENV安装与使用

    1.概述 pyenv 是一个python的版本管理软件,通过他,我们可以 方便的安装python 的版本,切换版本,解决版本不同带来问题. 2.安装pyenv 我们可以通过链接下载pyenv http ...

  4. Vue.js axios

    1.安装与引入 Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中,官方文档 在HTML文件中引入 <script src="https:/ ...

  5. GObject学习笔记(二)类型创建与注册

    前言 本文可在https://paw5zx.github.io/GObject-tutorial-beginner-02/中阅读,体验更加 在上一节中我们介绍了GObject类型的类和实例变量的创建和 ...

  6. 09C++选择结构(3)——教学

    一.求3个整数中最小值 (第20课 初识算法) 题目:输入三个整数,表示梨的重量,输出最小的数. 方法1:经过三次两两比较,得出最小值. a<=b && a<=c min= ...

  7. js 吸顶以及一些获取文档高度等小方法

    1.返回html文档元素document.documentElement 2.文档的高度document.body.clientHeight 3.html文档可视高度==页面可见区域的高度docume ...

  8. [.NET Blog] .NET Aspire 测试入门

    https://devblogs.microsoft.com/dotnet/getting-started-with-testing-and-dotnet-aspire/ 自动化测试是软件开发的重要一 ...

  9. ng-alain: Title Service

    文档地址:https://ng-alain.com/theme/title/zh 源码地址: https://github.com/ng-alain/delon/blob/master/package ...

  10. 渗透测试-前端加密分析之RSA响应加密

    本文是高级前端加解密与验签实战的第7篇文章,本系列文章实验靶场为Yakit里自带的Vulinbox靶场,本文讲述的是绕过请求包和响应包加密来爆破登录界面. 分析 这里的公私钥同上文一样是通过服务端获取 ...