线段树进阶
请确保您有一定的线段树基础中的基础。至少确保了解过并会实现模板题。
如果您的基础不太好,可以先看完基础内容部分。
本文部分链接无法打开,见谅!(其实本文应该也看不到的。但是我想着留篇遗产挂在 blog 上就没隐藏了)
线段树基础
基础操作包含:
1.基础的区间操作(修改+查询)
2.基础的单点操作(修改+查询)
3.维护一段连续的值(如最大子段和)
4.多个 lazy_tag 的操作
分析一些不易理解的地方。
1.线段树基础操作的复杂度为什么是 logn
单点操作显然正确。考虑区间操作即可。
观察到,线段树每层区间是连续的。区间操作的区间也是连续的。
当查询到某层时,中间部分一定会被包含,仅仅是左右两端的部分没被包含。
倘若中间部分被包含,那么在当前层之前一定会被访问到并停下。
所以每次只有两端会继续往下遍历。每次会往左右儿子遍历。然后一共 logn 层。所以最坏是 4logn 的复杂度。
2.线段树为什么开 4 倍空间
设 $d_i$ 表示每个点的度。
显然:$2d_2+d_1+1=d_2+d_1+d_0$,线段树是完全二叉树,无度为 1 的节点。
那么有 $d_2=d_0-1$,设线段树维护区间长度为 $n$,则 $d_0=n$。那么线段树有 $2n-1$ 个点。
在操作时可能访问最后一个点的左右儿子,即最大访问到 $2(2n-1)+1=4n-1$,那么开 4 倍空间即可。
3.多个懒标记如何下放
一些基本理解:多个懒标记即多种操作,一个 lazy_tag 只是考虑维护一种操作的总和,且 lazy_tag 是直接对数操作的。
举个例:加乘标记,例如操作是 +a*b+c,那么 add=a+c, mul=b,假设该数是 x,对该数进行先乘后加的懒标记操作后变为 x*b+(a+c)。
但实际结果应该为 (x+a)*b+c
考虑了某种下放顺序,但这样会有影响(如上面那个例子),消除影响,即可正确维护。
如何消除影响?在做出某种操作时直接修改标记。
举个例:在出现乘法操作时,对于先前的加法操作修改,使得之后的懒标记对数字直接修改是正确的。
出现 *b 时,让 add*b,即 add=a*b,那么最终 add=a*b+c, mul=b,对该数进行先乘后加的懒标记操作后变为 x*b+(a*b+c)。容易发现和实际结果一样。
如何一般化?考虑把多种懒标记融合在一个 push_down,一起下放,我们定优先级就能确定下放顺序了,顺便消除影响。
此时分离开 push_down 和一起 push_down 效果一样。因为没影响标记。只是标记下放的早晚罢了。
举个例:覆盖+翻转操作:定覆盖优先级更高。先下放覆盖。
1)每次有覆盖操作,需把先前的翻转操作清空。这点体现在 push_down 中 upd_cover 中。
对于 push_down 的理解:当前节点都覆盖,清空翻转标记了,把当前节点两个子节点的翻转标记肯定也要清空了。
2)由于每次操作都会顺路 push_down,保证了在翻转之前的覆盖标记,在下传时不会把该翻转标记清掉,因为此时的翻转标记还没打上。是先下放完后再打上的。
void upd_cover()
{
if (qL<=L && R<=qR)
{
push_down_rev();
...
}
push_down();
}
void upd_cover()
{
if (qL<=L && R<=qR)
{
...
}
push_down();
}
即以上两种写法一样。可以直接选择后者。
这是两种操作。如果三种操作呢?就比较复杂了,例如 P10639 吉司机线段树
先鸽着。
4.多种标记如何下放的技巧
把 push_down 当做二元组下放,如 (a, b) 下放到 (a2, b2),考虑先乘后加。那么就是 (a2, b2) = (a2b+a, b2b)。
5.对于维护左右前缀一类线段树的理解
1.在维护左右前缀时,没有合并条件可以考虑直接合并。取最优。因为不影响。
2.在区间查询时,有合并两颗线段树的操作,这启发我们只要信息能够合并(交换律+结合律),都可以考虑线段树维护。
6.懒标记的理解
有区修就考虑 push_down,所以要存懒标记并且及时下放。
但这不一定正确。学了扫描线后发现也不一定。下放懒标记是因为当前区间打了标记,但他管理的小区间没打上标记,以后查询有错,才会下放标记,消除错误的影响。
如果当前区间就算打了懒标记,对后面区间(或对求答案)也无影响,就无需 push_down。(即考虑小区间会对大区间有影响)
举个例:扫描线维护的 覆盖次数+覆盖长度,就没有 push_down 操作。如果下放了 覆盖次数,之后再取消覆盖是难维护的。于是考虑挂到当前节点不下放就好。
说到扫描线,这又启发我们,线段树维护的东西不一定是点,可以是线段。就如之后的扫描线,李超线段树。只不过侧重点在于,你想要什么维护什么为基础单位。
线段树例题
都是基础操作,但是比较有意思的题。
推式子
对于一类奇怪/不方便维护的式子,不妨先做些转换,推推式子再做。
展开方差公式,并重新推导,变成一个优美的容易维护的式子。
考虑弱化版。单点修改,是容易维护的。我们把区间修改转为单点修改,就是差分一下。
但是差分数组的 gcd 是原数组的 gcd 吗?推式子证明发现可行。
码农题
操作是容易维护的。但是码农。
这里给出写线段树的技巧:压行,减少没必要的代码。但是不要破坏代码的可读性。具体可以参考下此题代码。
思维题
仍然是先考虑弱化版,如果没有每个数只能取一次的限制就是最大子段和了。
然后再升级回强化版,有每个数取一次的限制呢?我们可以破除这个限制。
具体的,选择在每次遇到第一个前面出现过的元素,就把前面那个的贡献改为 -1,然后前面第二个出现的贡献改为 0。也就是支持单点修改
这样做也有问题:修改后再想在前面统计的答案是错误的。因此,我们继续消除影响,在每次修改贡献之前都更新一次答案,这样就避免了这种问题。
本题容易想到,先分讨区间无交集,是方便统计答案的。有交集呢?应该枚举中间断点,然后统计答案。但复杂度接受不了。
分类讨论的本质:正常需要枚举才能统计答案。但是发现枚举出来的结果有一些共性,我们可以对有相同共性的结果统一起来求,避免了枚举的复杂度。
分类讨论还要关注分讨的对象,如何分讨,全不全面。
于是分讨最大子段和的左右端点,会穿过哪些区间。然后就容易做了。
杂(水)题
发现维护的字符串只有 3 个字符,相当于三进制。我们可以把三进制拆成 2 个二进制。然后就容易维护了。这样极大节省了空间与码量。
水题。操作 2 可以转化成区间覆盖,但是如何确定覆盖长度?发现有单调性,二分就好。
扫描线基础
扫描线板子:扫描线(就是基于线段树的离线化算法)
一些理解:
1.线段树里到底存线段还是节点
看情况。比如维护面积并/周长并,需要合并两线段长度,所以以点为单位不好合并,以线段为单位就好维护(单位=叶子节点)
每个叶节点相当于存了 [L, R) 的线段,合并起来直接相加就好,很方便。
一些细节:
1.扫描线在排序时,一定要考虑在 x 坐标重合时,对操作先后顺序有排序。具体看题目而定。2.因为存的是左右两个线段,所以注意空间开 2 倍。
扫描线例题
巧妙转化题
转换一下,考虑星星能被哪些窗口框住。
注意,研究对象应该统一,如本题研究框住星星的窗口,是以星星为原点,窗口在星星的右上角,对象都是统一一个方向,右上角。而不能上下左右四个方向,不然无法求解。
所以维护一个最大值的扫描线即可。
以下 3 题可以直接看:[扫描线] 数据结构测试(2025.3.22)
CF377D,Gym 308768F,CF997E(第 3 题先鸽子)
吉司机线段树基础
咕咕咕。打算补一点
吉司机线段树例题
咕咕咕。打算补一点
李超线段树基础
咕咕咕。打算先不考虑。
李超线段树例题
咕咕咕。打算先不考虑。

浙公网安备 33010602011771号