线段树
线段树
1.引入
现在 lls抛给你一个问题:输入一个数列 需支持在线询问区间l - r的和。
显然这题可以用可爱的前缀和O(1)解决。
那lls加强题目:还要支持对第x个数加上一个数K,怎么办?
拜拜了您内我不做了
我们考虑对前缀和数组修改 但这样的复杂度将变成O(n)。 这种辣鸡复杂度能过的题就不叫数据结构题了
这时,我们就请出了它 -- 线段树
2.介绍
线段树是一种非常强大的树形数据结构,它可以维护各种乱七八糟的区间信息,它可以在O(logn)的时间复杂度内完成对一个线性序列的大部分操作,但前提是维护信息必须满足结合律。
线段树的每个节点都要保存一个区间的信息。
3.无区间修改的线段树实现
3.0 例题
3.1 存储与建树
3.1.1 分析
令当前节点编号为p 保存区间的左右边界分别为l,r 而对于一颗二叉树 我们还要知道它的左孩子和右孩子,我们采取堆式存储法: 则左节点为px2 右节点为px2+1(我一般提前define掉)
我们想知道一个区间的信息 可以利用分治思想 把它变成左右两部份 令mid=(l+r)/2
则左节点为保存区间的左右边界为(l,mid)
右节点为保存区间的左右边界为(mid+1,r)
那么对于信息的向上维护(从左右孩子推出父节点信息)也变得非常简单 只需对题目具体分析 然后利用结合律按题目意思模拟即可
这里再放张图照顾一下对式子过敏的同学:
3.1.2 代码实现
那么我们用结构体存储每个节点,再利用分治递归建树即可,不过结构体的数组一定要开四倍序列空间。
是不是很简单呢? 上代码!
现在你已经掌握了线段树的存储与建树!Orz
3.2线段树的单点修改与区间查询
3.2.1 单点修改
3.2.1.1 分析
对于单点修改,我们只需要在线段树上不断查找,直到找到一个叶节点保存的点恰等于需要修改的点,将它直接修改,并回溯,在回溯时向上维护即可。
3.2.1.2 代码
3.2.2 区间查询
3.2.2.1 分析
对于区间查询 我们只需在线段树上不断查找,若当前节点保存的区间包含在需查询的区间里 直接返回这个区间的值,否则就判断所查询区间是否与左子树区间有交集还是与右子树区间有交集(也有可能被同时包含了一部分)递归求有交集的几个部分的答案,将答案合并再返回即可。
3.2.2.2 代码
到这里,你已经实现了一颗难度较低的无区间操作线段树,赶紧拿它去切题吧!!
不过这只是线段树最简单的一部分,与树状数组的功能相当,接下来,更强大的,无法被树状数组取代的线段树要来了!
4.带区间操作的线段树
4.0 前言
这个部分的难度将有一个阶梯式上升,请反复阅读,保证对其理解透彻。 例题:我是例题
4.1 懒标记
4.1.1 引入
看到例题,它有一个操作令我们无从下手:将区间l - r加上k
于是你想:那么能不能用一个标记代替一个区间呢?
能!
不过我们要对它的定义稍作修改---
4.1.2 实现
每个节点的懒标记tag 表示这个节点所代表的区间还剩多少没有加。
这样一来 我们就只要修改tag 并在要对下一个区间做操作之前将下一个区间的值用tag修改并将它的tag也修改即可
这样,即不会影响正确性,也不会影响时间复杂度
所以我们要写的是三个操作:
PushDown : 下传标记
Modify:区间修改
Query:区间查询
(由于文字还是比较抽象,可参考代码理解)
代码环节!
注:spoon也是一觉醒来才对懒标记恍然大悟的,只需多思考,就能理解它
到这里:你掌握了基本的线段树功能,在做一些题时你也能灵活运用线段树,但还有许多更高深莫测的线段树算法等着你学习……
5.线段树的经典应用
5.1 逆序对计数
5.1.0 前言
题目传送门 在我们刚学 OI 时就遇见过这个问题,它的时间复杂度要求是 ,那时,我们利用归并排序的特性轻易地解决了了它,但由于归并排序的应用范围过于狭隘,现在,利用线段树来解决它反而成了对于每个看到这里的 OIer 最自然的想法。
5.1.1 分析
这个思路乍一看很难,但只要认真思考,对上文的理解深刻,还是能够自己想出来的。
我们先思考逆序对的形成缘由,有一个比你大的数比你先出现,那么你和他构成了一个逆序对,对于每个数,我们只要求出当前已经出现的数里有几个比他大即可。
反复阅读这句话,你想到了什么?
桶。
(意识流做题)
桶这个东西可以将大小关系转化成位置关系,这样一来,值为 val 的元素出现时,他对答案的贡献就是他后面所有桶里的数字之和。
看到这里有个区间求和。
线段树。
这时你说:“这道题 ai 最多有 10^9 ,内存直接炸飞啊”
看到重点部分的这个字眼“比我大”,想到了什么?
离散化。
而对于每个数的出现,只需要在桶中对应点加 1 即可。
综上,我们只需要写一颗支持单点修改和区间查询的线段树即可。
你要写树状数组我也不拦着你
5.1.2 代码:
5.1.2 后记
当然,这种用线段树维护桶的想法也叫权值线段树,我一般叫它树桶,它是一种非常灵活的思想,因为他可以做到删点和加点,你可以用它 AC 普通平衡树模板。
不过在这题里面因为离散化跑得比归并慢……
5.2 扫描线
5.2.0 前言
扫描线是一种高效的求解矩形面积/周长并的算法,可以利用线段树来提速。
具体地说,就是用线段树上的一个节点对应一段线段,并将矩形分割求解。
5.2.1 分析
咕咕咕
5.2.2 代码
6 主席树
6.0 前言
主席树,这个东西听起来高大上,其实就是一种奇技淫巧,把内存节省到能过题。
在阅览本文章之前,确保您掌握了普通线段树,否则建议学习这篇博客。(厚颜无耻)
6.1 基本操作的实现
板子 主席树与线段树真的大差不差,除了以下几点。
6.1.1 分析
这题我们要对每个版本建一颗线段树,但内存会爆,考虑利用主席树。
6.1.2 建树
令 rot_i 表示第 i 个版本的数组所构建的线段树的根节点。
注意,这里不能用堆式存储法,而是利用指针存储法来保存左右孩子。
建树代码可以说有手就行吧。
6.1.2 修改
我们可以向以其为修改基础的版本子树借用节点,以此节省空间。
6.1.3 查询
没什么好讲,直接找到对应版本号的子树上按线段树的查询方式查询好了。
最值
6.2 区间6.2.1 分析
如果,现在给你的是一颗权值线段树保存了整个数组,你会求全局 值吗
那我们是否可以利用前缀和思想,对于 它指向一颗段树保存了区间 中的信息的权值线段树的根,查询区间L,R就拿 tree_R-tree_L-1 ?
最后,开权值线段树记得离散化。