线段树操作
线段树二分询问
UVA - 11525 - Permutation,SPOJ - NKMOU - IOI05 Mountains,UVA - 12419 - Heap Manager
本质就是利用线段树是二叉树的性质,如果某个区间信息具有单调关系,那么就可以通过判断左右儿子节点中该信息的大小,判断进入哪个儿子节点。线段树的二分询问一般是要求整个区间上最左或最右侧的某个解,通过维护前缀信息或后缀信息实现(取出前k大的节点)。
区间上(下)界限制操作
洛谷 - P6242 【模板】线段树 3
区间上(下)界限制操作就是对区间 [l,r],将其中每个值进行上界为 v 的限制,即 ai←min{ai,v}, (i∈[l,r]),我们直接考虑究竟有哪些区间节点(树上节点)会被修改,为了使得修改的点数目可控,期望只有最大值(如果是下界限制就是最小值)被修改,而不是多种。
考虑记录下每个区间节点的区间最大值 mx1 和区间次大值 mx2,要求 mx2<mx1,如果该区间中只有一种值,那么 mx2=−∞。所以我们要修改的区间只有一种:mx2⩽v<mx1,考虑在目标区间 [l,r] 中通过递归搜索这样的区间,然后对其最大值进行修改,如果发现修改后 mx1=mx2,那么还需继续向下搜索更新 mx2。
实现方法
对于区间 [l,r] 的上界限制操作,具体实现方法如下:
- 在树上递归地找 [l,r] 的子区间 [l′,r′],满足 mx2⩽v<mx1(这里有一个剪枝,如果 v⩾mx1 则无需进一步递归其子区间,因为一定当前的上界 v 已经比整个区间的最大值都要大,不可能对任何节点的值进行限制;如果 v<mx2 说明需要进一步递归其子区间)
- 找到满足 mx2⩽v<mx1 的子区间后,将 mx1 减小到 v,如果发现 v=mx2,则还需进一步递归更新 mx2。
在代码实现中,其实第二步当 mx1=mx2 时,没有直接递归更新 mx2,而是等待下一次更新,如果有比 mx2 更小的上界限制,才进一步递归更新 mx2(这样可以少写一个更新函数)。
如果只有区间上界限制这一个操作,那么可以只记录一个 mxval 懒标记,用于将最大值直接限制到 mxval 上,如果和区间加法放在一起,那么就要在加上一个 addv 操作,不过这样做有些复杂,我们考虑将最大值限制转化为区间加法(只不过是只对最大值存在的区间进行加法),设一下两个区间操作:
add1
:区间最大值的区间加法懒标记。
add2
:区间非最大值的区间加法懒标记。
在 pushdown
操作中,我们只需判断当前节点的最大值是从哪个儿子转移上来的,由于当前节点的最大值可能已经被修改了,所以直接 mx1 = t[ls].mx1 + t[rs].mx1
,如果 mx1 = t[ls].mx1
说明是从左儿子转移上来的,于是用 add1
懒标记进行更新 t[ls].add(add1)
,否则使用 add2
懒标记进行更新 t[ls].add(add2)
。这里的更新操作 .update
就无需多言,和之前的区间加法无太大区别。
如果还要维护区间历史最大(小)值,那么还需额外记录一下两个:
add1_
:区间最大值的最大区间加法懒标记。
add2_
:区间非最大值的最大区间加法懒标记。
因为我们知道区间历史最大值就是记录下最大的懒标记就行,那为什么还要分最大和非最大区间呢?可以从下面例子中体会:(4|0
表示区间最大值为4,次最大值为0,其他节点类似,此处操作2后 4|4
,还体现出了最大值和次最大值延迟下传的情况)
时间复杂度证明
我们先按照上述实现方法进行操作,即进行上界限制后,仍要保持 mx2<mx1。
定理1:上界限制操作的均摊复杂度为 O(2nlogn)。(其中 n 为区间大小)
证明:这里从值域上进行证明,则值域大小的最大值为 n(每个值的初值均不相同),如果进行一次上界限制 v,则值域大小一定会减小,假设当前整个值域上的最大值为 mx1,可以画出以下值域的示意图:
不妨令 v<mx1(不然不会更新任何点),记区间中所有值在 [v,mx1) 中间的点的个数为 c,即
c=#{i:v⩽ai<mx1}
下面证明,该次操作的时间复杂度为 O(2clogn)。
-
若 v⩽ai<mx1,则最坏时间复杂度为单点修改,总共 O(clogn)。(如果某个区间节点的 mx2=v 则可以一直递归更新到子节点)
-
若区间节点的最大值为 mx1,记该区间节点的最大/次大值分别为 mx1/mx2,分两类情况:
i. 若 mx2⩾v 时,则该节点一定在 1 的某个叶子节点的更新路径上,用时包含在 1 中。
ii. 若 mx2<v 时,只需将 mx1/mx2←v/mx2,也就是只对最大值进行限制即可,因为限制后仍满足最大与次大值不同,所以无需进一步向下递归。这一部分的搜索,是 1 中搜索链中,每个点额外搜索的一个节点(因为每次向下更新会更新左右儿子节点,ii 的情况,只会出现在某个儿子节点中)
假设第 i 次操作修改了在 [v,mx1) 中的 ci 个节点,那么下一次的值域上界 mx1←v,由于值域大小至少为 1,所以
n−i=1∑m(ci−1)⩾1⇒i=1∑mci⩽n+m−1
又由于每次修改 ci 个点时间复杂度为 O(2cilogn),所以,总时间复杂度为
i=1∑m2cilogn⩽2(n+m−1)logn=O((n+m)logn)
区间历史和
洛谷 - U216697 线段树区间历史版本和,XJTUPC2023 - #1387. 大秦酒店欢迎您。
每个节点需要将当前和 sum
和历史和 sum_
区分开,并且同时维护这两个信息,其实可以容易想到 sum
的所有相关标记 sum_
一定至少要有,并且还要知道在标记下传前一共历史操作作用了多少次 tv
。
详细地说:使用区间历史懒标记 tv
来记录懒标记下传前有多少个区间和sum
没有更新到历史和sum_
中,由于本题还有对区间加法操作,所以需要区间加法的懒标记 addv
,对应历史区间加法懒标记 addv_
,因为要记录该懒标记有多少次没有更新到下面的区间中,这种多标记更新的方法写一个更新函数更加方便 update(k, k_, t)
表示区间加法修改量k
,历史加法修改量k_
, 区间历史懒标记t
,也就是有多少个当前节点的 sum
和 addv
还没更新到历史中去,最关键的就所有的历史更新addv_,sum_
是要优先于当前addv,sum
的更新之前:
线段树合并
主要在SAM的endpos集合合并上的应用:字符串 - 后缀自动机 - 维护endpos集合。
线段树模板
基本上所有线段树的信息都可以分为三个结构体,Info
节点信息,TNode
树上区间节点,SEG
线段树主程序。
静态线段树
一些基础操作练习题(前缀、后缀、最值、区间和维护等):
简单:UVA - 12299 - RMQ with Shifts - 单点修改,UVA - 1455 - Kingdom - 线段树区间修改+单点查询+并查集,UVA - 11992 - Fast Matrix Operations 线段树区间修改区间多目标查询
较复杂:UVA - 1400 - “Ray, Pass me the dishes!” - E10! 动态区间查询最大连续和
动态开点线段树
SPOJ - NKMOU - IOI05 Mountains,UVA - 12419 - Heap Manager,UVA - 1232 - SKYLINE
使用动态开点线段树一般是要满足一下两个条件:
- 有默认的区间初值(例如全部初始化为0)
- 区间大小 n 非常大(例如 109),动态开点线段树时间复杂度主要和操作数有关 O(mlogn)
可持久化线段树
在原数组的每个位置上都按照前缀和加入到该位置的线段树中,因为每个位置都是基于前面一颗线段树基础上加入一个新的值,并且如果没有修改的部分就原封不动继承下来即可,所以每次更新复杂度就是⌈logn⌉⩽log2n(树的高度向上取整),总共要加入n个节点,所以总时空复杂度上限就是nlog2n,动态开点的数组大小就开成nlog2n即可。有一些细节部分需要注意(主要是在指针使用时判断是否为空):