CF932D - Tree 题解
generated by Doubao
一、解题思路
(一)预处理的原因与方法
在这个问题中,我们处理的是一个不断添加节点的树结构,并且需要查询满足特定条件的节点序列。经过分析发现,每次添加新节点对之前已存在节点的贡献不会产生影响。这就意味着我们可以针对每个节点单独进行预处理,这样做能极大地提高算法效率,避免在后续操作中重复计算。
在预处理阶段,我们主要完成以下几个关键任务:
-
计算节点深度:对于每个新添加的节点
cnt,它的深度dep[cnt]等于其父节点p的深度dep[p]加 1,即dep[cnt] = dep[p] + 1。通过这种方式,我们可以构建出整个树的深度信息,这对于后续的倍增操作非常重要。 -
初始化倍增数组:
f[cnt][i]数组用于表示从节点cnt出发,向上跳2^i步后到达的节点。初始化时,f[cnt][0]就是节点cnt的父节点p,即f[cnt][0] = p。然后通过递推关系f[cnt][i] = f[f[cnt][i - 1]][i - 1],可以计算出不同跳跃步长对应的目标节点。g[cnt][i]数组用于记录从节点cnt向上跳2^i步过程中的最大点权。初始化g[cnt][0]为新节点的点权q,即g[cnt][0] = q,后续通过g[cnt][i] = max(g[f[cnt][i - 1]][i - 1], g[cnt][i - 1])来更新最大点权。
-
计算对数深度:
lg[cnt]数组记录每个节点深度对应的对数级别,用于控制倍增操作的范围。通过lg[cnt] = lg[cnt >> 1] + 1来计算,这种基于二进制移位的计算方式可以高效地得到每个节点的对数深度。
(二)寻找祖先中第一个比它大的点
根据题目中最长链的限制条件,对于每个节点 i,我们的核心任务是找到其祖先中第一个点权比它大的点的位置。这里我们采用倍增法来实现高效查找。
具体查找过程如下:
首先判断当前节点 cnt 的父节点 p 的点权 g[p][0] 是否大于等于当前节点的点权 q。如果满足条件,那么父节点 p 就是我们要找的祖先中第一个比它大的点,即 fa[cnt][0] = p。
若不满足条件,则从当前节点 p 开始,利用倍增数组进行查找。从最大可能的跳跃步长 lg[dep[p]] 开始,逐步减小步长。对于每一步长 i,判断从当前节点 now(初始为 p)向上跳 2^i 步后,是否满足深度条件 dep[now] >= (1 << i) 且跳跃过程中的最大点权 g[now][i] < q。如果满足,就将当前节点更新为跳跃后的节点 now = f[now][i]。当遍历完所有可能的步长后,最终得到的 now 就是祖先中第一个比当前节点大的点,将其赋值给 fa[cnt][0]。
(三)利用前缀和与倍增求解答案
在找到每个节点祖先中第一个比它大的点后,我们可以发现,最终所求的答案是从当前节点 i 开始往前的连续一段节点序列。为了快速判断一段节点序列的点权之和是否满足题目要求,我们引入了前缀和 sum 数组。
在添加节点时,sum[cnt] 记录从根节点到当前节点 cnt 的点权之和,计算方式为sum[cnt] = sum[fa[cnt][0]] + q,即祖先中第一个比它大的点的前缀和加上当前节点的点权。
在查询操作中,对于给定的起始节点 p 和点权上限 q,我们通过倍增的方式来寻找满足条件的最长节点序列。从最大可能的跳跃步长 lg[ans[p]] 开始,逐步减小步长 i。对于每一步长,判断从当前节点 now(初始为 p)开始,向上跳 2^i 步的这一段节点的点权和(通过前缀和相减得到,即 sum[now] - sum[fa[now][i]])是否小于等于给定的点权上限 q,并且当前节点的答案值 ans[now] 是否大于等于 2^i(确保有足够的节点用于跳跃)。如果满足条件,说明这一段节点可以加入到满足条件的序列中,那么我们更新剩余的点权上限 q -= (sum[now] - sum[fa[now][i]]),增加答案长度 res += (1 << i),并将当前节点更新为跳跃后的节点 now = fa[now][i]。不断重复这个过程,直到无法找到满足条件的跳跃步长,此时得到的 res 就是满足条件的最长节点序列的长度。
二、代码实现
code by hsy8116
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;
const int NR = 4e5 + 10;
long long last;
int lg[NR];
int dep[NR];
long long ans[NR];
long long sum[NR];
int f[NR][20];
long long g[NR][20];
int fa[NR][20];
int main()
{
int Q;
scanf("%d", &Q);
dep[1] = 1;
ans[1] = 1;
int cnt = 1;
lg[1] = 0;
while (Q--)
{
int t;
long long p, q;
scanf("%d%lld%lld", &t, &p, &q);
p ^= last;
q ^= last;
if (t == 1)
{
cnt++;
lg[cnt] = lg[cnt >> 1] + 1;
dep[cnt] = dep[p] + 1;
f[cnt][0] = p;
g[cnt][0] = q;
for (int i = 1; i <= lg[dep[cnt]]; i++)
{
f[cnt][i] = f[f[cnt][i - 1]][i - 1];
g[cnt][i] = max(g[f[cnt][i - 1]][i - 1], g[cnt][i - 1]);
}
if (g[p][0] >= q)
{
fa[cnt][0] = p;
}
else
{
int now = p;
for (int i = lg[dep[p]]; i >= 0; i--)
{
if (dep[now] >= (1 << i) && g[now][i] < q)
{
now = f[now][i];
}
}
fa[cnt][0] = now;
}
ans[cnt] = ans[fa[cnt][0]] + 1;
sum[cnt] = sum[fa[cnt][0]] + q;
for (int i = 1; i <= lg[ans[cnt]]; i++)
{
fa[cnt][i] = fa[fa[cnt][i - 1]][i - 1];
}
}
else
{
int now = p;
int res = 0;
for (int i = lg[ans[p]]; i >= 0; i--)
{
if (ans[now] >= (1 << i) && sum[now] - sum[fa[now][i]] <= q)
{
q -= sum[now] - sum[fa[now][i]];
res += (1 << i);
now = fa[now][i];
}
}
printf("%d\n", res);
last = res;
}
}
return 0;
}
- 变量定义:
NR定义了最大节点数为4e5 + 10。last用于记录上一次查询的答案,因为题目中查询操作的输入是经过与上一次答案异或处理的。lg数组用于存储每个节点深度对应的对数级别。dep数组记录每个节点的深度。ans数组存储以每个节点为起点的满足条件的最长节点序列长度。sum数组记录从根节点到每个节点的点权之和。f数组和g数组是倍增操作中用于记录跳跃节点和最大点权的数组。fa数组用于存储每个节点祖先中第一个比它大的点的信息。
- 输入与初始化:
- 读取查询次数
Q。 - 初始化第一个节点的深度
dep[1]为 1,答案ans[1]为 1 ,节点计数器cnt为 1 ,并将lg[1]初始化为 0 。
- 读取查询次数
- 处理查询:
- 每次读取一个查询,
t表示查询类型(1 表示添加节点,其他表示查询操作),p和q是经过与上一次答案异或处理后的参数。 - 添加节点操作(
t == 1):- 增加节点计数器
cnt。 - 计算新节点的对数深度
lg[cnt]、深度dep[cnt],并初始化倍增数组f[cnt][0]和g[cnt][0]。 - 通过循环计算不同跳跃步长下的
f[cnt][i]和g[cnt][i]。 - 寻找新节点祖先中第一个比它大的点,并更新
fa[cnt][0]。 - 计算新节点的答案
ans[cnt]和前缀和sum[cnt],并更新fa数组的倍增信息。
- 增加节点计数器
- 查询操作(
t != 1):- 初始化当前节点
now为查询的起始节点p,答案res为 0 。 - 通过倍增查找满足条件的最长节点序列,更新答案
res和当前节点now。 - 输出答案
res,并更新last为本次查询的答案。
- 初始化当前节点
- 每次读取一个查询,
通过以上思路和代码实现,我们可以高效地解决题目中树结构的节点添加和查询问题。在理解代码和思路的过程中,如果对某个部分还有疑问,可以进一步分析对应部分的逻辑,或者通过手动模拟一些简单的测试数据来加深理解。

浙公网安备 33010602011771号