线段树
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
如上图所示的线段树维护 区间。每个节点上的区间表示该节点维护的区间。
线段树的时间复杂度 (建树复杂度为 ),空间复杂度 。
单点修改操作
从根节点开始,选择涵盖目标节点的儿子往下跳,直到找到目标节点。修改数值,回溯更新数值。
如图所示,红色路径是更新 7 号节点的路径图。
区间查询操作
设节点 维护的区间是 ,左儿子、右儿子分别是 。
不难发现,当询问区间恰好为 时,答案即为 。
那如果不是呢?使用分治思想,将这个任务传给 的两个儿子,再从儿子那里接受答案。重复以上操作,直到询问被回答。
举个例子。假设我们询问 这个区间。
我们现在在 ,发现这个问题需要左儿子和右儿子一起帮忙才能回答,所以访问两个儿子。
对于 ,发现右儿子就是答案的一部分,返回右儿子的 ;
对于 ,左儿子和右儿子一起帮忙;
对于 ,发现该点就是答案的一部分,返回该点的 ;
对于 ,发现左儿子是答案的一部分,返回左儿子的 。
这个问题就这样被回答了。
如上图所示,绿色的点表示需要遍历儿子才能回答问题;黄色的节点表示直接返回该点数值;红色边是经过的边。
至此,我们已经学会使用线段树解决 单点修改、区间查询 问题了。
区间修改、单点查询问题
例题 1 您需要写一个数据结构,维护一个序列 ,支持以下操作:
- 将 的值加 ;
- 查询 的值。
考虑将此类问题转化成我们已经学会的“单点修改、区间查询问题”。
我们发现,给一区间内的所有数加 时,区间内相邻两数之差不变。所以区间加一个数等价于 修改边界处的差。
设 ,不难发现
所以,查询一个数 就被转化成求 数组的前缀和(连续的)。
那如果是区间修改、区间查询呢?
区间修改操作
回顾区间查询的过程。
总结发现,优化时间复杂度的方法是 不走到叶子节点,在非叶节点结束本次查询。
不妨尝试在区间修改中应用这个策略。与查询类似,我们给每个点设置一个值 ,表示这个节点维护的区间的每个点共有的数值。比如,给 里的每个数加上 ,那么我们可以给 这个点的 加 。
查询时,如果该节点是绿色的,则将他的 值下发给他的儿子。就像这样:
如果该节点是黄色的,直接累加 对答案的贡献,也就是 。
我们已经粗略介绍了线段树的基本操作。下面我们来看几道例题。
例题 2 已知一个数列,你需要进行下面两种操作:
- 将某区间每一个数加上x
- 求出某区间每一个数的和
区间修改、区间查询的线段树。
例题 3
一棵树上有 个节点,编号分别为 到 ,每个节点都有一个权值 。
我们将以下面的形式来要求你对这棵树完成一些操作:
I. CHANGE u t :把结点 的权值改为 ;
II. QMAX u v:询问从点 到点 的路径上的节点的最大权值;
III. QSUM u v:询问从点 到点 的路径上的节点的权值和。
先把这棵树按 序排列,拍成一个序列。这样,我们便发现:树上两点的路径在序列上是若干段的连续序列。
使用树链剖分 + 线段树维护即可。
例题 3 请求你维护一个数列,要求提供以下两种操作:
- 查询当前数列中末尾 个数中的最大的数,并输出这个数的值。
- 将 加上 ,其中 是最近一次查询操作的答案(如果还未执行过查询操作,则 ),并将所得结果对一个固定的常数 取模,将所得答案插入到数列的末尾。
记录一下现在队列的长度即可。详见这儿。
(未完待续 To Be Continued)