- admin 的博客
关于线段树上的一些进阶操作
- @ 2026-7-10 10:47:22
关于线段树上的一些进阶操作 更新于 2026/7/2 10:45:52 作者
command_block
前言
本文包含的内容有 :
-
李超树
-
历史标记
-
-
(未施工)
参考资料 :
-
国家集训队2016论文集 : 吉如一《区间最值操作与历史最值问题》
-
国家集训队2020论文集 : 李白天《浅谈函数最值的动态维护》
杂项
-
标记拼接
只是 pushup 的技巧,对于所有基于合并的数据结构都适用。
也可以理解为动态DP。
-
点积
维护两个序列 ,支持区间加,查询 的区间和。
设 分别为 的区间和。
则给 整体加 后, 。
可以扩展到其他的操作,就不再赘述。
线段树合并
其实就是空间换时间。
考虑总点数一直在减小,而初始时只有 个即可证明复杂度。
将二度点压缩后可以做到 空间,但一般不实用。
李超线段树
-
题意 : 要求维护一种数据结构,支持插入线段,查询某个 坐标处所有线段取值的 。
考虑使用线段树维护。我们称某个区间的“优势线段”为在 处取值最大的线段。
当某个区间插入一条新线段的时候,应该与原有的线段比较 处的取值,如果更优则该点新线段占领。
然而,这个区间内仍然有一部分位置,使得旧线段更优。
由于两条一次函数最多只有一个交点,我们能够发现,旧线段更优的部分必然完全在的左边或者右边。
那么我们把被击败的线段继续下放即可。
下放的复杂度是 的,而一条线段会被拆分成对 个点的直接操作,所以李超树的复杂度是 的。实际上常数很小。
如果维护的不是线段集,而是直线集,复杂度是严格 的。
首先在 LCA 处把路径拆一下,变成一条向上的和一条向下的。
显然,题目中加数的形式可以转化成有关深度的一次函数。
我们把树剖一下,每条重链采用李超树维护即可。
虽然这里的 “坐标” 不是连续的,但仍然是单调的,可以维护。不必动态开点。
对于区间 ,先维护子树 。注意李超树并不是 Leafy 的,更新信息要算上自身。
除此之外还有各个不在子区间内但能贡献的一次函数的 ,这类似于标记永久化。由于单调性,只需要取区间内的左右端点即可。
千万不要把维护线段的李超树当做凸包来思考!
虽然复杂度是 ,但是李超树和树剖的常数都很小,效率还行。
调了很久,友情提供样例 : Link
我们把拆除与不拆除翻转过来,先全部都拆除,再减去不拆除的贡献。
可以得到这样的方程 :
如果将 当做变量,可以整理成
其中,前者是关于目标的常量,可不计,后面则是一个一次函数。
我们发现,这些一次函数的斜率不单增,因此不能仅仅使用单队维护。这里可以使用李超树。
李超树维护直线集合是 的,而且常数比平衡树小,还好写。
设 为从 点到达叶子的最小花费。
显然有转移 。
我们可以把 点的转移变成形如 的一次函数,每次代入 求最小值。
这就可以使用李超树维护了。使用启发式合并,显然有复杂度 。
实际上,直接线段树合并,先合并两棵子树,然后合并根节点信息,由于线段只会被下放,总复杂度是 的。
前缀最值相关 (兔队线段树)
-
题意 : 平面上有 条线段 。
支持单点修改 ,查询从 能看到多少条本质不同的线段。
先不考虑修改,尝试求出静态问题的答案。
显然,若楼顶被挡住,其他部分不可能被看见,所以我们只需要考虑楼顶。
计算出每个楼顶与 连线的斜率,若某个楼顶的斜率比前面的都要大,则能被看见。
也即 : 斜率前缀严格最大值的个数。
考虑线段树维护,把某个区间当成完整的子问题。
为区间 的前缀最大值个数, 为区间最大值。
在合并两个区间的时候,左边的最大值可能会把右边本来能看见的一部分楼房挡住。
设 表示 区间中 的部分的前缀严格最大值的个数。
则合并 产生的前缀严格最大值的个数为
考虑如何求出 ,可以直接利用线段树的信息来求解。
若到达叶节点,简单比较大小即可。
若 ,左区间的贡献为 , 此时有 。
若 ,则该点上记录的右侧前缀最大值必然均 ,不必再次计算。
此时
不难发现上述算法总是只会递归一侧,复杂度是 的。
总复杂度为 。
兔队指出,上述操作中涉及到了一个减法,若信息不满足可减性,则无法维护。
所以,考虑对各个区间 改而维护 : 考虑整个 时 内的前缀最大值数量。记为 。
我们仍然沿用 的定义,则有 :
-
-
$mx[lc]<x\Rightarrow c\big([l,r],x\big)=c\big((mid,r],x\big)$
-
$mx[lc]\geq x\Rightarrow c\big([l,r],x\big)=s[u]+c\big([l,mid],x\big)$
答案不能直接取用,可以使用 得到。
Seg-beats
有时候,题目中的操作会使信息量趋向于减小,此时可能产生均摊的做法。
前面的几道均摊小例题并非正统 Seg-beats 内容,较为熟悉的读者可以跳过。
题意 : 给出一段长度为 的序列,要求支持如下操作:
-
区间开根
-
查询区间和
大小为 的正数在开根 次后会变为 。 开根总是 。
我们在线段树上暴力 ,若发现某个区间内有大于 的数才进入。
由于不会在中途停止,到达一次叶节点对应 的复杂度。
若到达叶节点,必然造成一次开根。总共的开根次数是 次的。
总复杂度也就是 。
题意 : 给出一段长度为的序列,要求支持如下操作:
-
区间加
-
区间整除
-
询问区间
-
询问区间和
接下来介绍一种典型的势能分析方法 :容均摊。
即 : 找出一种能概括信息量的“特征值”,证明其消长和时间消耗有关,最终通过求和得到复杂度。
为了行文方便,下文将某节点点包含的信息复杂度称为“容”。
本题中,我们将一个节点的容定义为该节点内数字的极差。
若极差为 ,则代表所有的数字都相同,则可以批量操作并打下标记(可以转化成加法标记)。
若极差不为 ,则暴力递归下去。
一次除法操作显然可以至少让极差除以 。对于某个节点,除 次就能让容变为 。
任意的整体操作不会影响容的大小。
然而,一次“非整体”的任意修改操作会导致某个节点的容(不可预期地)变化。
一次区间操作最多提供 次“非整体”的修改操作。
也就是说,每次区间修改操作提供了 的额外代价。
总复杂度 。
然而,这是不严谨的,如果考虑下取整产生的误差,可能有除法之后极差不变的情况。
如 ,在除以 之后变为 加上 之后又变回 。
这样,每次除法操作都需要暴力,复杂度退化。
我们发现,下取整最多带来 的偏差,所以极差为 时才有可能除完极差不变。
我们只需要特殊处理极差为 的情况,方法有很多。
最粗暴的方法就是记录此时两种数分别是什么,以及个数,不过这样不优美。
观察发现此时这两种数的加减变化量是相同的,我们可以在每次除法时判定,若区间内所有数除完之后加减变化量相同,则用加法标记代替。
题意 : 在 P4145 的基础上,加入区间加操作。
类似地,我们将一个节点的容定义为该节点内数字的极差。
若极差不为 ,一次整体开根操作可以至少让极差开根。
证明 : 设原来的极差由 产生,而开方后的极差为 ,由 且 ,不难推知 。
需要开根 次才能让容变回 。每次区间修改操作提供了 的额外代价。
总复杂度 。
这同样是不严谨的,下取整仍然可以造成偏差,如 开根之后变为 ,极差不变。
类似地应用加法标记即可规避。
题意 : 给出一段长度为的序列,要求支持如下操作:
-
区间对某个数取
-
查询区间和
-
查询区间最大值
我们发现所有数值只会变小,即信息量减小。
考虑维护最大值 ,最大值的个数 ,以及严格次大值 ,区间和 .
对于一个修改 ,如果 ,显然无影响。
如果 ,那么只会对最大值产生影响,利用个数 来计算贡献,并打下标记。
如果 ,这次操作可能影响到最大值之外的数,我们无法在当前节点处理,只能向深处 dfs ,直到能处理为止。
复杂度看起来玄学,实际上是 的。
定义某个点的容为其中数的种类数。所有点的种类数和是 的。
如果在 dfs 中需要经过,则至少会将该点最大值和次大值合并使得容减少 。dfs的额外贡献就是 的。
- 加入区间加操作
仍然考虑维护上述四个量 。
把标记改进成 的形式,意义为先加上 再对 取最小值。
取 修改时仍然按照上述规则大力 dfs。
根据论文中的证明,复杂度有上界 ,但实战中的表现近似于 。
-
题意 : 给出一个排列 ,对于每个 ,询问当保留 的数时,该序列笛卡尔树(大根堆)的子树大小和。
首先分析一下性质,笛卡尔树的建树可以认为是每次抽取最大值分治。
那么,从 出发,设左边第一个大于它的位置为 ,右边则为 。
那么, 一定比 先选取,将 与其他区域分隔开了。
现在, 是区间 最大值,那么整个区间一定都在其子树内。
这就证明了 的子树大小是 。
令 从小到大增加,维护 和 的和即可求出答案。下面只讨论 。
每次会增加一个比先存所有数更大的数,那么其会令左侧的 对其位置取 。
但是注意,还空着的位置是不会计入下标的,我们其实是在 “插入”。
那么,右侧的所有标号都会增加 ,即对右侧的 区间加 。
区间取 ,区间加,维护总和,直接套用上一题的做法即可。
还有一个要注意的是,由于我们在使用线段树,所以不可能真正的插入,而是预留好空位每次填掉。
这样以来,区间操作时,仍为空位的点需要被忽略。
使用维护点积的技巧,将空位的权设为 即可。
- 同时
题意 : ①区间对某数取 。②区间对某数取 。 ③查询区间和。
夹来夹去,容终究还是会变小的,不会变大。
考虑再维护最小值,次小值然后讨论即可。复杂度仍然是 。
- 总结
上文的几个算法中,我们通过暴力 ,把取 这一操作,转化成了若干对最大值的加法操作。
我们可以对最大值和其他数分别维护两套标记。
-
维护两套标记的好处
题意 : 维护一个序列 ,支持 :
①区间对某数取 。②区间对某数取 。 ③区间加。
有另一个序列 ,如果某次修改中 有变化,则 加一。④询问 的区间和。
如果是区间加操作,整个区间的 都会改变,也就是 都加一。
对于 操作,转化为若干对最大/小值的加法操作,结合最大/小值个数,对 的影响仍然容易算。
注意最大值和最小值可能相同,此时需要特判避免重复贡献。复杂度仍然是 。
-
「二重奏」
题意 : 维护两个序列 ,资瓷
①将 对 区间取 。②将 对 区间取 。
③将 区间加。 ④将 区间加。 ⑤询问 的区间最大值。
只有一个数组的时候,我们把值划分成最大值和次大值,现在有两个数组,我们可以进行类似的划分。
某个位置 : 同时为 的最大值 / 只为 的最大值 / 只为 的最大值 / 其他数。
维护四套标记就好了。 个数组则需要 套标记。
历史值问题
历史值 : 在维护序列 的同时,在每次操作后,序列 会对序列 的对应位置产生相应贡献。
-
历史版本和 : 每次操作后 。
-
历史最大值 : 每次操作后 。
-
「线段树基础题」
题意 : ①区间加。 ③查询区间最大值。 ④查询区间历史最大值。
我们用标记来解决这类历史值问题。
- 在线段树标记的下推机制中,某个点存有标记,表示整一棵子树在标记存在的时间内都未曾更新。
于是,问题的核心就在于分析单个节点上停留的标记的影响。
在非历史值问题中,我们只关注该节点“当下”的标记是什么,所以我们会直接将标记合并起来。
但在历史值问题中,我们不仅要考虑该节点现在的标记合并结果,还要考虑历史上推来的(按时间为序的)每个标记的依次作用。
为了便于理解,我们不合并标记,假设每个节点上有一个队列(按照时间先进先出),放着所有曾经推过来的标记。
下推标记时,将该节点上所有的标记推到两个儿子处,并清空队列。
对于每个节点,维护 分别表示 : 区间最值, 区间历史最值。
每次有一个区间加 标记推来时,令 然后 。
这样做的正确性是显然的,但实际上我们不可能真的存下各个标记。
于是,我们寻找一种方法 概括一个队列的标记对当前节点的影响。
先思考对一个点打若干次标记的情形。
设推来的加法标记分别为 ,其前缀和为 。
则打上第 个标记之后, 的值为 。所以, 的值为
于是,只需记录 就能得知该标记队列对节点的影响。
合并加法标记的方法时简单求和,所以前 个标记合并后恰好等于 ,那么 可以表述为标记的历史最大值。
接下来考虑两个队列如何合并,设为 ,合并后的结果为 。
注意到 $S_3[i]=\begin{cases}S_1[i]&(1\leq i\leq k_1)\\S_1[k_1]+S_2[i-k_1]&(k_1+1\leq i\leq k_1+k_2)\end{cases}$
我们需要快速得到 $\max_{i=0}^{k_1+k_2}S_3[i]=\max\Big(\max_{i=0}^{k_1}S_1[i],\ S_1[k_1]+\max_{i=0}^{k_2}S_2[i]\Big)$ 。
维护 即可,这也正是目前加法标记的值。
具体地,设 为合并后的加法标记, 为加法标记的历史最大值。
每次 下推的时候,令:
v.m=max(v.m,v.x+u.mt);
v.mt=max(v.mt,u.mt+v.t);
v.x+=u.t;
v.t+=u.t;
u.t=u.mt=0;
-
题意 : ①区间加。 ②区间赋值。 ③查询区间最大值。 ④查询区间历史最大值。
原来曾经是黑题,现在因为技巧进一步普及,掉紫了。
若不考虑赋值操作,则转化为上一题。下面考虑赋值操作。
在经典线段树题目中,偶尔可以不思考两个标记的互相作用,直接令两个标记互斥。
然而,在维护历史版本的线段树问题中,需要完整地考虑每个标记的历史影响,则不能采用这种手段。
现在在我们的标记队列中,存在两种标记。两种标记混杂的情况并不便于处理。
可以发现,若存在一个赋值标记,则之后整个区间都是同一个数,那么加法标记可以等价地转化为赋值标记。
于是,该标记队列等价于一个加法标记队列紧跟着一个赋值标记队列。加法标记队列用前文的方法处置。
对于赋值操作 ,最终产生的历史最大值为 ,记录下这个即可。意义为“赋值标记的历史最大值”。
具体操作可见代码。
使用 维护 的区间和。
把区间取 划分成若干对区间最大值的加法操作。
我们维护两套标记,一套只对最大值生效,另一套对所有数都生效。
注意,只对最大值生效的标记下推时要判断儿子是否含有同样的最大值。
历史最大值的合并是要讲究顺序关系的(因为标记队列是有顺序的),所以标记影响的对象必须不交才方便维护。(难以处理多个标记队列)
所以,我们让另一套标记对最大值之外的数(而非所有数)生效。
有若干坑点 :
-
判断儿子是否含有最大值时,要在两个儿子之间比较。
-
若儿子不含有最大值,需要对其(略小的)最大值打上对非最大值生效标记。
具体细节见代码。
- 历史版本和
记原序列为 ,版本和序列为 。
可以把 “更新 序列”也看作一种标记,每次操作后给整棵树打一个。
考虑一个更新标记和加法标记交替出现的队列,记当前节点区间和为 ,历史版本和为 ,区间长度为 。
加法标记 会令 。更新标记会使得 。
考虑标记队列
记 为前 个标记中,加法标记的和。
则对 的总贡献为 $\sum\limits_{i=1}^{k}\big[q[i]=upd\big]s+S_t[i]*len=s\Big(\sum\limits_{i=1}^{k}\big[q[i]=upd\big]\Big)+len*\sum\limits_{i=1}^{k}\big[q[i]=upd\big]S_t[i]$
于是,我们只需记录 和 。
意义是更新标记的总个数,以及每个更新标记打上时, 的和,即加法标记的历史版本和。
若两个队列 拼接得到 ,更新标记的总个数容易计算。
$$\begin{aligned} &\sum\limits_{i=1}^{|q_1|+|q_2|}\big[q_3[i]=upd\big]{S_3}_t[i]\\ =&\sum\limits_{i=1}^{|q_1|}\big[q_1[i]=upd\big]{S_1}_t[i]+\sum\limits_{i=1}^{|q_2|}\big[q_2[i]=upd\big]{S_2}_t[i]+{S_1}_t[|q_1|]\\ =&{S_1}_t[|q_1|]\Big(\sum\limits_{i=1}^{|q_2|}\big[q_2[i]=upd\big]\Big)+\sum\limits_{i=1}^{|q_1|}\big[q_1[i]=upd\big]{S_1}_t[i]+\sum\limits_{i=1}^{|q_2|}\big[q_2[i]=upd\big]{S_2}_t[i]\\ \end{aligned}$$于是,再记录 即合并后的加法标记,则可以合并。
具体地,设 为加法标记, 为加法标记的历史版本和, 为更新标记个数。
当 更新时。
v.ms+= u.upd*v.s + u.mt*v.len;
v.mt+= v.tg*u.upd + u.mt;
v.upd+= u.upd;
v.s+= u.tg*v.len;
v.tg+= u.tg;
-
题意 : 一个多重集合的价值定义 : 将其变为公差为 (给定常数) 的等差数列需要插入几个数字(无法实现则价值为)。
多次询问 的子区间的价值和。
若可以变为公差为 的等差数列,则称该区间合法。
不难发现,若区间 合法,那么其所有子区间都合法。
我们从每个右端点 找到最靠左的 (这可以双指针求出),所有这些极长区间的子区间的并集就是所有合法的区间。
对于一个合法的区间 ,其价值容易计算 :
考虑离线,从左到右移动右端点,并维护每个左端点对应的答案。
当 增加时,首先要删掉不合法的部分,这里可以暴力单点删除。
接下来,分别维护 。
和 比较显然。前者相对下标不变,可以在双指针移动时处理。后者则每移动一次 触发 。
和 是类似的,只讨论后者。
维护单调栈,栈中两个元素之间的左端点对应的 是相同的,贡献可以看做区间加。
而单调栈的变化次数时均摊 的,则会产生 个区间加法。
将右端点移动到 时维护左端点的线段树状态称为版本 。
设 为区间 的价值。
若我们需要求出 ,则在版本 做区间求和即可。
我们要求是 则要在 版本同时做区间求和,不难转化成历史版本和问题。
将线段树可持久化可以支持强制在线。
题意 : ①区间减 并对 取 。 ②区间加。 ③区间赋值。
④单点查询。 ⑤单点查询历史最大值。
可以设计出一种标记 ,表示先加 再对 取 .
不难发现题目中的所有操作都可以用这个标记描述 : ①,②,③。
标记合并的时候,原有 与 合并,则变为 。
现在考虑历史最大值如何维护。由于是单点查询,只需把标记推到叶子,我们只需要对标记做手脚。
历史最大值实际上就是标记前缀和的 。只要能求 ,和前面的题目维护方法是相同的。
按照值域考虑每个数接受标记之后会变成什么,发现是一个如下图所示的折线:
(讲到这里,人人都要画一样的图,我就剻了个图来, )

两个标记的 就是这两个函数的 。不难发现 $\max\big((a,b),(c,d)\big)=\big(\max(a,c),\max(b,d)\big)$。
让最终的标记对叶节点的数作用一次,则得到答案。
题意 : 单点修改,询问 的不同的后缀最小值个数。允许离线。
可以直接沿用「楼房重建」的做法,然而复杂度 太差,无法获得满分。
注意到每次询问的都是后缀,而且可以离线,考虑如何利用这两条性质。
有如下暴力 : 从末尾向前扫,求出后缀最大值,若其改变则答案。
可以按数组下标从后向前扫,以时间为下标,设枚举到第 个位置, 表示当前位置在第 个时刻的后缀最大值。
我们发现,某个修改操作的影响,是给某个时间区间 (该次修改生效的区间) 取 。
查询相当于问某个位置被更改过多少次,不难使用数域划分后的历史版本和标记维护。
具体实现中,对于一个针对最大值的标记,需要记录标记本身被修改的次数。
非常卡常,这可能是差评数量多的原因吧……具体技巧见代码。
题意 : ①区间加。 ②区间取。③查询区间最小值。 ④查询区间历史最小值。
此时若沿用标记 ,则合并时要对标记取 ,就发生了下面的事:

可以看到我们并没有让信息量减少。
考虑转而使用容均摊数域划分,变为若干个给最大值的加法操作,就易于维护了。