我不会数据结构
线段树 / 树状数组 / KDT / 李超线段树
概述:线段树用于维护序列,是一个包含 个节点的树形结构, 其中非叶子节点都有两个儿子,中序遍历这颗树得到的恰好是 。
一般在存储时,空间会开到 倍,有一种特殊的写法可以使空间优化到 倍 :即线段树上 所对应的区间为 , 读者可以自行证明其正确性。
一般我们使用的线段树为狭义线段树(标准线段树),它要求节点 的左儿子是 , 右儿子是 。
线段树的基本性质:
- 任意两个节点的区间要么互相包含, 要么不交。
- 区间 可以唯一拆分成线段树上若干个区间的不交并, 称这些区间构成 的区间拆分。
- 最小区间拆分的大小不超过 ,其中 为常数。
线段树基本操作:
我们可以用线段树维护一系列区间数据结构问题:
-
最简单的是区间查询, 即询问给定序列的 的相关信息, 若我们对于每个区间维护一个 , 使得 和 容易计算,并且我们可以快速合并 , 设合并一次的复杂度为 , 则进行一次查询的复杂度为 , 初始化复杂度为 。
-
维护区间和 : 。
-
区间乘积 : 。
-
区间最大子段和:额外维护 , 表示前缀 / 后缀最值,则 。
-
-
当进行区间修改时,我们维护了懒标记:对于每个点额外维护了一套标记信息 , 使得在修改 时我们不需要暴力修改, 而是先将修改的信息保存在 ,算出他对 的影响。 此后无论是修改还是询问, 只要经过这个点我们都对标记进行下放,并把 清空。
在设计标记时,要注意标记之间能否快速合并, 是否能够快速算出对 的影响。
-
区间乘,区间加,我们维护 $g(l,r) = (mul,add)$ 表示区间先乘上 $mul$ 在加上 $add$,而不维护先加上 $add$ 再乘上 $mul$。
- 区间加, 区间 次方之和: ,维护区间 次方之和后组合数算一下即可。
- 区间加,区间选取 个数乘积之和:设 为 节点选了 个数的总和, 转移直接卷积即可。 区间加时,有 ,时间复杂度 。
-
维护矩阵乘积:
有时线段树维护的区间信息满足一定的线性性和递推性,可以直接用矩阵表示转移,维护的 看做行向量, 转移看成 的矩阵, 常见的结合律有乘法对加法, 对加法,异或对与。 也称为动态 dp。
我们通过几道例题来实际感受一下它的用途:
一棵树,支持单点修改权值,维护全局最大独立集大小。
朴素 dp 设 表示 子树内, 是否选择的最大独立集大小, 有转移 :
, ,
再考虑把树剖分, 设 为 的重儿子, 表示 不考虑重儿子时的 dp 值, 将状态转移方程改写:
, , 表示成矩阵 :
, 注意此时要求 和 在同一条重链上,于是我们直接线段树维护这个矩阵即可。
修改路径时, 我们只用考虑轻儿子对父亲的贡献, 也就是一条重链的链顶对父亲的贡献,这是可以直接对父亲的转移矩阵操作的。
[THUSCH2017] 大魔法师
维护一个序列 , 支持以下 7 种操作:
- 区间 , 区间 , 区间
- 区间 加 , 区间 乘 , 区间 赋值为 。
- 询问区间 之和。
直接用记下 即可,转移易。
-
小 Y 和恐怖的奴隶主
dp 直接写成矩阵即可。
-
CF750E New Year and Old Subsequence
dp 直接写成矩阵即可。
可持久化线段树 / 标记永久化
如果一棵线段树在修改时我们要维持改版本之前的线段树不变, 我们需要对需要修改的线段树节点复制后再修改, 称为可持久化。
单点修改的可持久化是简单的:我们只需要把根到叶节点的点全部复制一遍即可,因此修改 次的空间复杂度为 。
但当要支持区间操作时,每次 push_down 时都要新建儿子, 这会使空间大大消耗,此时我们考虑标记永久化,直接将标记放到对应的位置后不下传, 只在询问时计算它的贡献。
要满足标记要有可交换律。
例:主席树区间加,应该这么写:
inline int update(int p, int l, int r, int ql, int qr, int v) {
int P = ++tot;
tre[P] = tre[p], tre[P].sum = tre[p].sum + 1ll * (min(qr, r) - max(ql, l) + 1) * v;
if(ql <= l && r <= qr) {
tre[P].tag += v;
return P;
}
int mid = l + r >> 1;
if(ql <= mid) tre[P].l = update(tre[P].l, l, mid, ql, qr, v);
if(qr > mid) tre[P].r = update(tre[P].r, mid + 1, r, ql, qr, v);
return P;
}
inline ll Query(int p, int q, int l, int r, int ql, int qr) {
if(ql <= l && r <= qr) return tre[q].sum - tre[p].sum;
int mid = l + r >> 1;
ll res = 1ll * (min(qr, r) - max(ql, l) + 1) * (tre[q].tag - tre[p].tag);
if(ql <= mid) res += Query(tre[p].l, tre[q].l, l, mid, ql, qr);
if(qr > mid) res += Query(tre[p].r, tre[q].r, mid + 1, r, ql, qr);
return res;
}
应用
总长度为 , 字符集大小为 的子序列自动机, AC 自动机。
子序列自动机:前一个位置的转移只由后一个位置加一个单点修改。
AC 自动机:一个点的转移只在其失配指针基础上修改它有出边的儿子。
区间数颜色:维护 表示上一个颜色和 相同的点, 则 的颜色数等于 中 的个数。
区间 mex:对于每一个 维护 , 答案就是第 棵主席树中第一个 的点, 线段树二分即可。
可持久线段树的最常见用法是充当一个线段树的前缀和。
当询问满足可减性时, 进行 的询问时用第 棵树减去第 棵树,例如权值线段树可持久化后得到主席树, 可以支持区间 大。
当询问是在线时, 一般使用主席树使问题降一维。 若可以离线则可以树状数组。
[AH2017/HNOI2017]影魔
势能分析
正常的线段树当 就会停止, 但某些问题我们有可能到了 依然无法快速更新,这时线段树的时间复杂度需要依靠势能分析。
例:[雅礼集训 2017] 市场
维护一个长度为 n 的序列,支持 m 次操作,操作包括区间加,区间除一个数下取整,以及查询区间最小值和区间和。
直接用线段树记录区间最大最小,向下取整时,若 , 即差值相等时,直接打标记退出。
[BZOJ 5312] 冒险
维护一个长度为 n 的序列,支持 m 次操作,操作包括区间按位或一个数,区间按位与一个数,以及查询区间最大值。
用线段树记录区间的与, 或和,计算出区间不是所有元素都有的位置, 当按位与 / 或时完全包含这个区间或交为空集时直接打上区间与 / 或标记, 因为此时值的相对大小不会改变。
【模板】线段树 3
维护一个长度为 n 的序列, 支持区间加, 区间对一个数取 min,区间和, 区间最大值, 区间历史最大值,区间历史和。
合并与分裂 / 优化 dp
合并即是合并若干棵总点数为 的动态开点线段树, 可以直接按叶子个数启发式合并, 但我们可以直接合并。
定义 Merge(x,y) 表示合并 x 和 y 子树。
- 若 x = 0, 返回 y;若 y = 0,返回 x。
- 若两个点都是叶子,直接合并信息。
- 否则,递归合并 merge(lsx, lsy), merge(rsx, rsy), 然后将 x 和 y 的信息合并。
分裂依葫芦画瓢就行,可以看看代码:
inline int merge(int x, int y, int l, int r) {
if(!x || !y) return x + y;
if(l == r) return t[x].siz += t[y].siz, addr(y), x; int mid = l + r >> 1;
t[x].l = merge(t[x].l, t[y].l, l, mid), t[x].r = merge(t[x].r, t[y].r, mid + 1, r);
t[x].siz += t[y].siz, addr(y); return x;
}
inline void split(int &x, int &y, int l, int r, int ql, int qr) {
if(ql <= l && r <= qr) return y = x, x = 0, void();
int mid = l + r >> 1; if(!y) y = New_node();
if(ql <= mid) split(t[x].l, t[y].l, l, mid, ql, qr);
if(qr > mid) split(t[x].r, t[y].r, mid + 1, r, ql, qr);
t[x].siz = t[t[x].l].siz + t[t[x].r].siz, t[y].siz = t[t[y].l].siz + t[t[y].r].siz;
}
[HEOI2016/TJOI2016]排序
有一种简单的二分 + 线段树 的做法, 使用线段树合并 + 分裂可以做到在线 并求出所有区间。
做法:直接二分第 位的值, 将 的位置设为 0, 的位置设为 1, 容易发现当 x 小于 p 位的真实值时为 , 否则为 , 然后区间排序就变成了区间 0 / 1 赋值。
做法:发现当对 排序后,若我们知道排序是从大到小还是从小到大, 再加上 的所有值域范围, 则 可以直接确定。
注意这个排序有点像珂朵莉树中的"推平"操作, 于是我们直接对每一个有序的连续段维护一个值域线段树, 则排序只用把 所有连续段线段树合并一下即可, split 操作可以通过线段树分裂实现。
由于推平连续段总复杂度 , 线段树合并 + 分裂为 , 故总时间复杂度 。 感觉珂朵莉树还可以和其他连续段数据结构套起来。
[NOI2020] 命运 & [PKUWC2018]Minimax
这两个题都差不多,讲一个就行了。
设 表示 的权值为 (离散化后的) 的方案, 显然有转移:
当 为叶子, ; 当 一个儿子:;
当 两个儿子:
维护一下 的前后缀和即可,当 和 有一个为 0 时直接打上标记即可。
[PKUSC2019]树染色
同样有 , 限制可以将 的子树合并完后置为 0 即可,最开始有个全局加 1。
注意此时的线段树的非叶节点是有信息的,它表示 的值都是相同的,所以在 push_down 时若没有儿子要新建儿子, 与上面维护的要区分, 上面的非叶节点的信息只是为了 push_down 给叶节点。
转移就显然了:若 x 为一个连续段, 直接打上 的标记, 若 为一个连续段,还需将 取反后加上 再与 x 相乘。 维护一个加乘标记即可。
[九省联考 2018] 秘密袭击 coat
神题。
建议配合 拉格朗日插值食用。
所有连通块的 大显然不可做, 考虑转化:
若一个连通块的 大为 , 则我们在 都对这个连通块算 1 的贡献, 这样显然是正确的。 而 的实际意义就是有多少个连通块, 其 大 。
考虑和 排序 一样的思路,将 的设为 1, 否则设为 ,则问题就转化为有多少个连通块, 其权值 ,对于一个 , 我们显然可以 计算。总复杂度 。
考虑优化,数据范围只要我们优化一个 或 成 即可。
设 ), 对于一个 , 我们算的是 。
拿这个直接取做线段树合并显然是不行的, 因为 是平移操作 ,线段树没办法平移。
继续转化, 将限制缩小, 设 表示 子树内 的多项式, 转移还是一样,设 表示 子树内所有 之和, 即 , 答案就是:
(想一想,为什么要这么转化?)
显而易见的是对于每一个 , 和 都是关于 的不超过 次的多项式,我们考虑直接把 的多项式带入 , 然后 拉格朗日插回系数。
考虑 时怎么快速求值,再回顾一下我们的转移:
,
这时我们线段树的下标 p 就表示 和 (都是点值),那么就相当于最开始在 x 的线段树,下标在 的位置 乘 , 然后合并时 全局加 1 后 F 对应位置相乘, G 对应位置相加,最后把 加到 , 这怎么做?
考虑这么多操作, 都是线性的,那就用矩阵来表示!
直接维护 的矩阵复杂度直接爆炸,考虑优化,发现:
于是只用保存 四个状态即可,初始向量为 。
嗯,很好, 比暴力慢了 6 倍。
结构分析
在使用线段树维护区间操作时,设操作 可以根据标记作用的不同类型来将影响到的区间划分成以下五种:
- 被 完全包含,此时一定存在标记。
- 位于被包含区间的内部,没有影响。
- 在寻找区间时经过的点,一定没有标记。
- 在寻找区间时经过的点下传标记影响到的点,若它的祖先链上存在一个有标记的点,则它一定有标记。
- 位于第 4 类区间的内部,没有影响。
若要动态维护线段树上每个区间的标记个数,可设 表示 是否有标记, 表示 到祖先链上是否有标记,转移为:
- 第一类:。
- 第二类:。
- 第三类:。
- 第四类:。
- 第五类:。
例:
[ZJOI2020] 传统艺能
[ZJOI2019]线段树
区间单调栈
以一道例题作为引入:
LG4198 楼房重建
维护长度为 的数组,支持单点修改,查询全局不同前缀最大值个数。
若利用线段树直接 push_up,需要记录当前区间的答案,最大值,但合并两个区间不好合并,因为要查询左侧最大值为栈顶时的可能的右端点个数。
设 表示区间 中 的不同前缀最大值个数,等价于 。 而 的求解仍然可以在线段树上递归:
- 若当前区间最大 ,返回 0。
- 递归到叶子节点,简单判断即可。
- 若左侧 ,将答案加上 ,递归左子树。
- 否则直接递归右子树。
这样,我们通过将 push_up 以一个 的代价完成了维护。
容易发现这利用了前缀最大值的可减性。
若不利用减法,还可以这样维护:
树状数组
李超线段树
KDT
本文作者:henrici3106
本文链接:https://www.cnblogs.com/henrici3106/p/16747839.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步