线段树学习笔记
待补:扫描线,可持久化,树套树,李超线段树,线段树分治,势能线段树,树链剖分。
线段树学习笔记
线段树(Segment Tree)是一种基于分治思想的二叉树,能够高效的维护一些区间信息。与树状数组相比,线段树显得更加通用。
线段树是一棵完全二叉树。且根节点是唯一的,代表的区间是整个维护的范围,如
线段树有以下性质:
线段树的每个节点维护的区间的大小之和的数量级为
显然,线段树每向下走一层,区间长度都会缩减一半,则树高为
节点的数量级为
对于每一层,显然节点个数都是上一层的两倍,则共有
Tips:线段树用结构体书写程序效率更高,也更好调试。
例题
给定长度为
的数列 ,需要支持两种操作:
- 将下标在
的数加上 - 查询区间
的和。 数据范围:
, , ,
线段树的建树
既然线段树使用递归定义的,那当然也要用递归实现啦。
先处理子节点,然后再将子节点的信息合并到当前节点上。
示例代码:
void pushup(int u) {
tr[u].v = tr[u << 1].v + tr[u << 1 | 1].v;
}
void build(int u, int l, int r) {
if (l == r) {
tr[u].v = a[l];
return;
}
int mid = (l + r) >> 1;
build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
pushup(u);
}
线段树的单点修改
操作形如
可先在线段树上找到维护的区间为
由于每个节点维护的区间是确定的,递归修改即可,别忘了更新当前节点的信息。
示例代码:
void upd(int u, int l, int r, int k) {
if (l == r) {
tr[u].v += k;
return;
}
int mid = (l + r) >> 1;
if (k <= mid) upd(u << 1, l, mid, k);
else upd(u << 1 | 1, mid + 1, r, k);
pushup(u);
}
线段树的区间查询
操作形如
先说过程,向下递归找区间,判断一下再向下走,就不会出现无交的情况了。如果是被完全包含就直接返回,否则继续递归。
一种严谨点的方法是分类讨论,但很麻烦。
考虑到每次向下递归时,那一层都只会有两个节点会继续向下递归,而其它节点要么无交,要么被包含。又因为树高为
还有一种更感性的方法,。将区间在
示例代码:
ll query(int u, int l, int r, int L, int R) {
if (L <= l && r <= R) return tr[u].v;
int mid = (l + r) >> 1;
ll res = 0;
if (L <= mid) res += query(u << 1, l, mid, L, R);
if (mid < R) res += query(u << ! | 1, mid + 1, r, L, R);
return res;
}
区间修改
虽然支持单点修改的线段树有点用,但用处还是十分局限,所以出现了延迟标记(也称懒标记或 lazy-tag)。
延迟标记的意思就是修改了过后,只要不再次更新或查询,就不会告诉子节点被修改,但是该维护信息是修改时就更新了的。
考虑到延迟标记,每次非完全包含时都需要下传标记,注意有时候需要开 long long。
用 pushdown 函数完成下传:
void pushdown(int u, int l, int r) {
if (tr[u].tag) {
int mid = (l + r) >> 1;
tr[u << 1].tag += tr[u].tag, tr[u << 1 | 1].tag += tr[u].tag;
tr[u << 1].v += (mid - l + 1) * tr[u].tag;
tr[u << 1 | 1].v += (r - mid) * tr[u].tag;
}
}
现在线段树的基本操作已经结束,模板题自己写去吧。
普通线段树的一些应用:
例题
给定长度为
且初始值均为 的数列,有 次操作。
要求支持:
- 将
间的数全部取反,即 变为 , 变为 。 - 查询
内所有数的和。 数据范围:
, 。
容易发现,该问题的具有“可合并性”,考虑使用线段树维护。
每次修改时,将区间的值变为
例题
给定长度为
的序列 ,有 次操作。
要求支持:
- 求
的最大子段和。 - 将
修改为 。 数据范围:
, , , ,时限 330ms。
发现最大子段和具有可合并性,容易想到线段树。
每个节点维护的信息除了该区间的答案
每个信息的合并如下:
修改的过程相似,不加赘述。
例题
给定长度为
的序列 ,有 次操作,要求支持:
- 将区间
的数开平方根后向下取整。 - 查询下标在
的数的和。 数据范围:
,
由于我太菜了,所以不讲解势能分析法。
发现不好对区间修改的延迟标记进行操作,但发现一个数最多进行
然后就很美妙了,对于每个修改直接向下暴力递归即可,当区间的最大值为
时间复杂度:
证明:将数
证毕。
例题
给定长度为
的数列,和模数 ,有 次操作:
- 将区间内的每个数加上
。 - 将区间内每个数乘上
。 - 求序列的区间和对
取模的答案。 数据范围:
, 。
一道简单的题,维护多个标记即可,注意标记下传的顺序。
权值线段树:
即维护值域的线段树。
例题
给定长度为
的数列 。
求出该中逆序对的数量。
其中逆序对指满足且 的二元组 的数量。 数据范围:
,
既然以值域作为维护区间。肯定要先离散化。
从前往后扫描
动态开点线段树:
比如值域为
如果直接建一棵权值线段树,空间直接起飞。但是发现有很多的节点都是没用的,于是就产生了动态开点线段树。即根据每次所需建立新的节点,只是不满足完全二叉树的编码方式了而已,记录一下左儿子和右儿子的编号即可。
有了动态开点线段树,就能使线段树进行可持久化操作了。
可持久化线段树:
例题
给定长度为
的数列 ,有 次操作,要求支持:
查询区间的第 小值。 数据范围:
, , ,
考虑一个很暴力的想法?对于每一个点都建立一棵前缀的权值线段树,对于操作可以在两棵树上同时递归,因为将两棵树相减就得到了当前区间所构成的权值线段树。但显然,空间无法承受。发现相邻的两颗线段树只多有
具体地,对于点
然后实现就很简单了,注意单次修改要增加
核心代码:
void change(int ltr, int &x, int l, int r, int k, int v) {
if (!x) x = ++cnt;
if (l == r) {
tr[x].v += v;
return;
}
int mid = l + r >> 1;
if (k <= mid) {
tr[x].rs = tr[ltr].rs;
tr[x].ls = ++cnt;
tr[tr[x].ls] = tr[tr[ltr].ls];
change(tr[ltr].ls, tr[x].ls, l, mid, k, v);
} else {
tr[x].ls = tr[ltr].ls;
tr[x].rs = ++cnt;
tr[tr[x].rs] = tr[tr[ltr].rs];
change(tr[ltr].rs, tr[x].rs, mid + 1, r, k, v);
}
pushup(x);
}
int query(int x, int y, int l, int r, int k) {
if (l == r) return l;
int mid = l + r >> 1, tmp = tr[tr[y].ls].v - tr[tr[x].ls].v;
if (tmp >= k) return query(tr[x].ls, tr[y].ls, l, mid, k);
return query(tr[x].rs, tr[y].rs, mid + 1, r, k - tmp);
}
另外:可持久化线段树还有很多其他的功能,比如实现可持久化数组,还有可持久化并查集等等。
咕咕咕。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具