浅谈线段树
1 前言
线段树一直是高频考点,可以直接出也可以作为数据结构优化其他算法。这里我只想说说线段树的基本理解以及如何构造,也就是如何写出信息和标记,信息之间的合并,标记之间的复合,信息和标记之间的复合。以及矩阵的辅助理解,区间最值、历史版本相关问题。
2 线段树
线段树运用了分治的思想,将每个大区间分成两个小的区间。每个区间都有自己的信息,为儿子两个区间信息的合并。
2.1 单点修改#
如果我们有这样一个问题:
- 单点加
- 区间和
在线的情况下,如果是暴力,单点修改是
这是单点修改的情况。但出现区间修改,定位到每个点的话,单次复杂度就是
2.2 区间修改#
我们先需要找到线段树上所有完全包含于目标区间的极大区间,修改这些即可。此时要是能用线段树,信息要能够直接计算出来。至于上面的节点回溯时合并信息即可。
如果所有询问都与这些极大区间以下的节点无关,那么答案还是正确的。
但是下面的节点怎么办?它们没有立马被修改操作所影响。此时可以在极大区间上打一个标记,表示该区间以下的所有节点都没有被标记所影响。
如果询问或修改下面的节点,就把标记下传,同时更新出应有的信息,此时的答案和修改就是正确的。
这样就将修改均摊给了所有询问。但此时也面临着问题。我们需要找到一种标记的描述,使得任何时刻它都等价于还未下传的修改造成的影响,下传后能够直接计算出子节点当前的信息。同时,如果下传时遇到了另一个标记(此时一定有先后顺序,由标记的下传的时刻可以得出,即下传的一定在后面),那么两个标记要能够复合成一个,这个新的标记等价于两个标记先后对区间的影响。
如果能够找到这样的标记,可以与信息复合、与标记复合,那么就可以用普通线段树实现。
2.3 标记与信息#
抽象的说,信息与标记都是一个半群,设信息为
有些题目难在信息的合并,有些难在标记的复合。而标记的复合一般来说会更难,因为并不是简单的相加,而是将两个标记融为一体,如果你的标记内容过少,就会难以复合成新的标记。
信息一般至少要有询问的答案,而复合一般是增量。举个不算难的例子:
- 区间加
- 区间乘
- 区间覆盖
- 区间询问和
先考虑加乘。显然信息就是区间和。那么标记可以只维护单单一个增量吗?难以复合。因为加乘会相互影响,具体的:
为什么是先乘后加?
再考虑覆盖操作,只用一个标记
tag merge(tag x, tag y) { //b->a
if(x.c) { //如果前面有覆盖操作,那么整个操作就可以看成复合
x.c = x.c * y.a + y.b;
} else {
x.a *= y.a;
x.b = x.b * y.a + y.b;
}
if(y.c) x.c = y.c; //如果后面有覆盖,最后就覆盖了
}
2.4 矩阵#
如果能够将信息看作向量,每一个修改看作一个矩阵,那么用线段树直接维护矩阵是第一选择。为什么可以这样做?因为矩阵天然满足结合律,所以所有标记的复合就是矩阵乘法,不需要思考。
普通的矩阵是
所以说矩阵是方便我们推式子和理解,写的时候不一定要直接写。
例题:P4314 CPU 监控 、[NOIP2022] 比赛。
3 区间最值相关
一个例题:
- 区间取最小值
- 区间加
- 求区间和
这类问题的特点就是修改操作有区间取最值,定位到的区间无法快速统计信息。考虑维护区间最大值
- 如果
,那么没有任何影响,退出。 - 如果
,那么唯一修改的就是所有最大值,此时维护一下区间最大值数量 就可以快速维护总和。 - 否则继续向下递归。
可以证明这样的复杂度是
这样做,标记怎么构造?我们将数分为最大值和非最大值,每一部分有不同的增量,分别维护即可。具体的,维护
void pd(int u) {
int mx = std::max(t[u << 1].mx, t[u << 1 | 1].mx);
if(mx == t[u << 1].mx) {
mdf(u << 1, add1[u], add2[u]);
} else mdf(u << 1, add1[u], add2[u]);
if(mx == t[u << 1].mx) {
mdf(u << 1 | 1, add1[u], add2[u]);
} else mdf(u << 1 | 1, add1[u], add2[u]);
}
如果询问多一个区间最大值,因为操作
这里包含最值操作的主要思想就是将最值操作转变为了加减操作,是我们熟悉的。
4 历史版本问题
每次询问不止问当前的信息,还需要询问历史信息相关。可以看作多了一个数组
通常维护的历史版本信息有历史最大值、历史最小值、历史版本和。
而历史最值的维护方法一般要维护最大/小版本增量标记。
4.1 不含最值操作#
询问是历史最大值、最小值,可以用普通线段树维护,如 P4314 CPU 监控。
对于修改是区间加减,求历史最大值的和、历史最小值的和、历史版本和。这里有三种转化方法。
4.1.1 历史最大值#
定义数组
那么维护
4.1.2 历史最小值#
同理,定义数组
复杂度
4.1.2 历史版本和#
这个根本不涉及最值操作,直接打标记就可以做了。复杂度
4.2 含有区间最值操作#
如果含有区间最值操作,就用吉司机线段树将最值操作化为区间加减问题,就可以打 tag 维护了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具