1 条题解
-
0
一、先搞懂“广义线段树”是啥(和普通线段树的区别)
普通线段树的每个非叶子节点,一定把区间从正中间劈开(比如[1,4]拆成[1,2]和[3,4])。但广义线段树不一样,它的非叶子节点可以“随便拆”,只要满足
左区间的右端 < 右区间的左端就行(比如[1,4]可以拆成[1,3]和[4,4])。但不管怎么拆,广义线段树的总节点数是固定的:
2n-1个(n个叶子节点,对应n个原始元素;n-1个非叶子节点,负责拆分区间)。二、步骤1:构建广义线段树(把树的结构“存下来”)
要处理后续查询,得先把这棵树的结构记录清楚,包括每个节点的:
- 对应区间(比如节点5对应[2,3]);
- 左孩子、右孩子(拆分后的两个子节点);
- 父节点(用来算深度和LCA)。
怎么构建?(用“栈”模拟,避免递归栈溢出)
因为n可能很大(2e5),递归会栈溢出,所以用栈迭代的方式构建:
- 初始化栈:把根节点(对应区间[1,n],父节点为0)压入栈。
- 循环弹出栈顶元素:
- 如果是叶子节点(区间左端点=右端点):直接记录它的区间和父节点,不用再拆分。
- 如果是非叶子节点:按题目给的“划分位置m”(n-1个m,按节点标号递增顺序),把当前区间[L,R]拆成左区间[L,m]和右区间[m+1,R]。然后:
- 给当前节点分配一个标号,记录它的区间[L,R]和父节点;
- 先把右孩子压入栈(因为栈是“先进后出”,这样左孩子会先处理,保证“先序遍历”的顺序);
- 再把左孩子压入栈。
- 重复步骤2,直到栈空,树就建好了。
三、步骤2:预处理“深度”和“LCA”(为算距离做准备)
节点之间的距离公式是:
距离 = 深度[u] + 深度[v] - 2×深度[LCA(u,v)]。所以得先算出每个节点的深度,以及任意两个节点的最近公共祖先(LCA)。1. 算每个节点的深度(用BFS)
- 根节点(1号)的深度是0;
- 用队列遍历:每个节点的左/右孩子的深度 = 父节点深度 + 1,依次记录所有节点的深度。
2. 算LCA(用“倍增法”,快速查询)
LCA是两个节点“最近的共同祖先”。比如节点A在第3层,节点B在第4层,它们的LCA在第2层,那A到B的路径会经过LCA。
倍增法预处理:搞一个“跳步表”
up[k][u],表示节点u往上跳2^k步到达的祖先(比如k=0是跳1步,即父节点;k=1是跳2步,即祖父节点)。- 先填k=0的层:
up[0][u] = 父节点u; - 再填k≥1的层:
up[k][u] = up[k-1][up[k-1][u]](跳2^k步 = 先跳2^(k-1)步,再跳2^(k-1)步)。
查LCA的过程:
- 先把两个节点拉到同一深度(比如u深、v浅,就把u往上跳,直到和v深度相同);
- 再让两个节点一起往上跳,直到相遇,相遇的节点就是LCA。
四、步骤3:处理查询(分解区间+算距离和)
每个查询给
u, l, r,要做两件事:1. 分解区间[l,r],得到S[l,r](找最少的节点)
目标是找最少个不重叠的节点,让它们的区间拼起来正好是[l,r]。用栈迭代分解:
- 初始化栈:把根节点(1号)和目标区间[l,r]压入栈。
- 循环弹出栈顶元素(节点x,目标区间[cur_l, cur_r]):
- 如果x的区间和[cur_l, cur_r]完全不重叠:跳过;
- 如果x的区间完全包含在[cur_l, cur_r]里:把x加入S[l,r](这就是我们要的节点);
- 否则:x的区间部分重叠,需要拆成左、右孩子处理,先压右孩子,再压左孩子(保证左孩子先处理)。
- 重复直到栈空,S[l,r]就得到了。
2. 算距离和
对S[l,r]里的每个节点v,用公式
距离 = 深度[u] + 深度[v] - 2×深度[LCA(u,v)]算出距离,所有距离加起来就是答案。样例拆解(帮你理解每一步)
看样例输入:
10 3 1 2 9 6 4 5 7 8 3 7 6 7 18 4 5 14 5 6输出是
7、11、3。以第一个查询7 6 7为例:- 分解[6,7]得到S[l,r](假设是节点A和节点B);
- 算u=7到A的距离,加u=7到B的距离,总和是7。
具体数值怎么来的?靠前面的步骤一步步算:树的结构构建后,每个节点的深度、LCA都能确定,代入公式就得到结果了。
- 1
信息
- ID
- 4247
- 时间
- 3000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 11
- 已通过
- 1
- 上传者