线段树/树状数组/分块 例题
Contents
主要记录一些遇到的线段树/分块例题。
例1 CF438D
题意
给定
其中,
题解
本题分块和线段树都可以做,我们这里用 线段树 来做。
主要是需要考虑 区间取模 怎么办?
回忆一下分块例题中的 区间开方,我们维护了一个额外的tag表示这个区间是否为 全0/全1,如果不是 全0/全1 就暴力开方。
取模操作同理,我们发现,如果
所以,我们维护一个 区间最大值,取模时,检查一下 区间最大值是否大于
- 如果大于
,就继续往下递归。 - 如果小于
,就直接返回。
base case 就是区间长度为
例2 CF558E
题意
给定一个长度为
输出所有询问结束后的string。
其中,
题解
线段树来处理。
首先,string只包含小写字母。所以每个 node
可以维护一个 cnt[26]
代表这个node里的每个字母出现的次数。
其次,对于排序,我们在每个 node
中维护一个标记
最后,维护一个 node
而言,若 node
必然是排序好了的!(要么
有了以上信息,我们就可以进行 sort
操作了!
sort
- 提取出
内每个字母出现的次数。 - 求出
与 (当前node 左child的范围)的区间交集 - 求出
与 (当前node 右child的范围)的区间交集 - 用指针遍历
(或者 ),根据升序/降序 将 字母出现的次数分别填充 到 左child和右child的cnt[]
中。(注意,这里的填充是指:先填充进一个int* buf = new int[26];
的动态数组,然后将buf[]
作为参数,再往下传递,直到区间完全覆盖,再将buf[]
的内容复制进cnt[]
里)。
代码
|
|
例3 洛谷P1972 HH的项链
题意
给定一个长度为
其中,
题解
我们可以发现,如果我们固定了询问的右端点
例如
由上,在处理 区间内不同的数 时,一个常见的套路是:
- 离线处理所有询问,按右端点
排序。 - 从左到右遍历数组,遍历到
时,对于所有 的 copy,仅保留最靠右的那一个(也就是 ),之前的所有 copy 全部删除。 - 回答所有
的询问。
那么对于本题,第一步是离线处理询问,按右端点
第二步是遍历数组,遍历过程中维护一个 int pos[]
数组,其中 pos[val]
代表:在 val
的数 最靠右的 index。
当我们遍历到 int val = arr[i]
,将 pos[val]
处的数字 删掉(更新线段树),然后将 pos[val] = i;
•本题中,删掉就是将 线段树的位置
然后回答所有 以
注:如果询问 在线 怎么办?可以使用主席树!(对于
pr[i]
建主席树)具体做法参考:CF1422F Boring Queries
代码
|
|
例4 CF1000F One Occurrence
题意
给定一个长度为
其中
题解
和例3类似的套路,也是离线处理询问(根据右端点sort),从左往右遍历,仅保留最靠右的复制。
问题在于怎么删除 和 加入数字?因为本题不再是求数量了,所以不能简单的加
会发现,我们仅关心一个区间内是否存在 unique 的数字,对于一个询问 pos[val]
是否小于
那么,我们用线段树维护一下 区间最小值 即可,其中 pos[arr[i]]
的最小值。如果一个区间
那么,删除位置
加入位置 pos[arr[i]]
。
代码
|
|
例5 CF803G Periodic RMQ Problem
题意
给定一个长度为
然后回答
其中,
题解
注意到数组的总长度可以达到
但是注意到,这个数组是有初始值的,按理说应该把树建好,不能动态开点。
所以我们考虑 不建树,而是在开点的时候,把这个点的初始状态处理好(就是在没有任何修改的情况下,这个点的初始状态),然后再对这个点进行操作。
所以,对于一个点对应的区间
注意到,由于数组是一个循环节,所以可以分以下三种情况:
的长度 ,它的初始最小值就是数组 的最小值。 的长度 ,且属于同一个循环节,那么用 ST表 预处理一下 的最小值即可。 的长度 ,且不属于同一个循环节,那么它的初始最小值就是
开点的函数是代码里的 build()
,注意到只要我们访问到了 cur
,并且需要访问它的任意一个 child 时,需要把左右两个子树都开点,这保证了 pushup()
的正确性。
代码
|
|
例6 CF gym103687F. Easy Fix
题意
给定一个 permutation
定义
给定
其中,
题解
先讨论一下交换后有哪些元素受到影响?
很明显,
我们先讨论
Case1: 如果
Case1.1: 如果
Case1.2: 如果
Case2: 如果
Case2.1: 如果
Case2.2: 如果
然后再讨论一下
只要先把
• 需要注意当
好的,现在该讨论一下怎么获得
首先,获得
然后
再讨论一下怎么处理询问?
我们将 Case1, Case2 分开处理:如果我们开一个线段树/树状数组,index 为
每次询问就是询问一个区间
似乎是 主席树?
确实,不过本题询问离线,Stupidcdd 教导我:
能用主席树做的,离线情况下就能用线段树/树状数组。
回忆一下主席树的作用:对于每个区间
那么询问离线的时候,我们只有一个树状数组,可以在遍历的过程中,获得前缀
因为树状数组的历史版本保存不下来,不妨从询问下手?
离线的一个常见套路:
对于询问的端点
,每个端点开一个 vector 储存所有的询问,遍历到这个点时处理所有对应的询问,加到答案上。
这个题就是这样,我们想要知道
|
|
在这个题中:
我们通过离线询问,答案相减来实现。 我们通过权值线段树/权值树状数组来实现。- 树状数组上面维护了 Case1.1, 1.2 (或者 2.1, 2.2) 的情况。
总结一下思路:
离线的关键在于从左向右遍历的时候,获得每个前缀对应的树状数组。
在离线 ans[]
进行操作。
代码
|
|
例7 Atcoder ABC237G. Range Sort Query
题意
给定一个长度为
给定
所有询问结束后,回答
其中,
题解
经典套路题。一般看见这种每次询问然后排序的,一般来讲只有在整个数组中 distinct 的数非常少的时候才能做。
这个题注意到我们只关心
而除了
所以我们可以直接开两棵线段树,线段树里只储存
而 sort 的时候,我们就相当于把两棵线段树的
代码
|
|
例8 Eerie Subarrays
题意
给定一个
其中,
题解
假设我们正在考虑的数是
将所有
求
的数量,使得 (等价于使得 )。
线段树没法统计这个,但可以用分块来统计!
同时注意到给定的是一个
这样每次更新只需要更改两个位置的数,而更改一个位置的数会影响后面的所有的
最后如何统计答案?对于块内,暴力统计,对于一个整块,我们维护块内每个元素出现的次数即可。
如何维护?显然不能对每个块都开
代码
|
|