线段树.cpp
前言
本人采集,2023 年 CSP 一个奖没拿(),所以有什么问题欢迎联系我斧正。(luogu @_IceCream_)
如果您是数据结构大师,可以直接看后面的ex线段树部分。
线段树的基本操作
概念及其复杂度
例 0.
给你长度为
这时候暴力或者差分及前缀和都是
那么现在最主要的问题是,怎么样将一个序列映射到一棵树上?
映射很好办,我们将每一个区间用点表示。若点
最后出来的树就是这样的,这玩意就叫线段是。若采用堆式存储(编号为
但是 OI-wiki 给出的证明是要开
分析:容易知道线段树的深度是
的,则在堆式储存情况下叶子节点(包括无用的叶子节点)数量为 个,又由于其为一棵完全二叉树,则其总节点个数 。当然如果你懒得计算的话可以直接把数组长度设为 ,因为 的最大值在 时取到,此时节点数为 。
当然个人推荐开
建树
还是
我们从根节点
这里建议将合并写成函数,会方便许多。
void pushup (int node) {tree[node] = tree[node << 1] + tree[(node << 1) + 1];} // 这里注意加法和位运算的优先级
void build (int node, int l, int r) {
if (l == r) {
// 若自己就是叶子节点,赋值返回。
tree[node] = a[l];
return ;
}
int mid = (l + r) >> 1; // 分成两个子节点下传。
build (node << 1, l, r);
build ((node << 1) + 1, mid + 1, r);
pushup (node); // 别忘记上传
}
可以知道我们最多访问了
单点修改
设当前遍历的区间是
,此时位置在左子节点对应的区间中,所以向左遍历。 ,此时位置在右子节点对应的区间中,所以向右遍历。
举个例子,我们要把
那么抽象成未知的,再写成代码就是这样的。
void modify (int node, int l, int r, int s, int c) {
if (l == r) { // Step 3. 找到单点
tree[node] = c;
return ;
}
int mid = (l + r) >> 1;
// 按 mid 划分左右区间,分别判断遍历。
if (s <= mid) modify (node << 1, l, mid, s, c);
else modify ((node << 1) + 1, mid + 1, r, s, c);
pushup (node);
}
叶子节点深度不超过
区间查询
因为我懒单点查询跟单点修改思路几乎一致,所以这里不再过多赘述。
假如当前我们遍历到的区间是
,此时 在 中,正好是我们要求的答案,直接返回; ,也就是 在 中,此时查询区间在左半部分的区间中,向左遍历; ,也就是 在 中,此时查询区间在右半部分,向右遍历; ,此时 两个部分中均有要查询的元素,所以两边都要遍历。
不妨简化一下,我们发现
- 若
,直接返回节点值; - 若
,遍历左区间; - 若
,遍历右区间。
long long query (int node, int l, int r, int s, int t) {
if (s <= l && r <= t) return tree[node]; // 第一种情况
int mid = (l + r) >> 1;
long long ret = 0;
if (s <= mid) ret += query (node << 1, l, mid, s, t); // 第二种情况
if (t > mid) ret += query ((node << 1) + 1, mid + 1, r, s, t); // 第三种情况
return ret; // 别忘了返回答案
}
我们最多可以将
区间修改与懒惰标记
若我们想要区间修改,最先想到的就是用
遍历的过程跟区间查询差不多,如果遍历到了要修改的点就修改权值并挂标记即可。
若点
我们称它叫做懒惰标记,还可以把要修改的权值给懒标记的权值,这样修改起来更方便。
这个东西需要考虑的细节有点多,这里列举出来。
- 函数内部遍历时下传顺序先于遍历
- 需要考虑修改的运算跟整体(大小)有无关系,例如加法,就需要在修改节点权值是乘一个
- 注意任何修改都需要上传更新
- 修改后别忘了清除标记
long long laz[N];
void pushdown (int node, int l, int r) {
int ls = node << 1, rs = (node << 1) + 1, mid = l + ((r - l) >> 1);
if (laz[node]) {
long long x = laz[node];
tree[ls] += x * (mid - l + 1), tree[rs] += x * (r - mid); // 注意点 2
laz[ls] += x, laz[rs] += x;
laz[node] = 0; // 注意点 4
}
}
void modify (int node, int l, int r, int s, int t, long long c) {
if (s <= l && r <= t) {
tree[node] += (r - l + 1) * c;
laz[node] += c;
return ;
}
int mid = l + ((r - l) >> 1);
pushdown (node, l, r); // 注意点 1
if (s <= mid) modify (node << 1, l, mid, s, t, c);
if (t > mid) modify ((node << 1) + 1, mid + 1, r, s, t, c);
pushup (node) // 注意点 3
}
由于线段树的适用性极高,这使得它在不同的题目上可能会有不同的应用,从而被 OIer 们开发并延伸出了很多变形与操作。
ex线段树
动态开点线段树
动态开点线段树是针对于线段树空间大这一缺点进行优化的。实际上这玩意还是传统线段树。
字面意义,动态开点的核心思想正是在有需要的时候才新建节点。而且我们不用
单次操作复杂度仍是
int build (int l, int r) {
int node = ++tot; // 改动 1
if (l == r) {
tree[node].val = a[l];
return node;
}
int mid = l + ((r - l) >> 1);
// 改动 2
tree[node].ls = build (l, mid);
tree[node].rs = build (mid + 1, r);
return node;
}
小清新线段树
有长度为
小清新线段树,你可以理解为带剪枝的线段树,也可以说是时间复杂度分析和懒标记的灵活应用的一类传统线段树。
例题中如果我们直接单点开平方的话,复杂度飙升到
注意到
那么我们可以再开一个变量记录区间中的最大数,若当前区间最大数
void modify (int node, int l, int r, int s, int t) {
if (l == r) {
tree[node] = sqrt (tree[node]);
mx[node] = sqrt (mx[node]);
return ;
}
int mid = l + ((r - l) >> 1);
if (s <= mid && mx[node << 1] > 1 /*若 max > 1 则继续暴力遍历*/) modify (node << 1, l, mid, s, t);
if (t > mid && mx[(node << 1) + 1] > 1) modify ((node << 1) + 1, mid + 1, r, s, t);
pushup (node);
}
可以发现这玩意跑的非常快,时间复杂度的话均摊一下复杂度就还是
吉司机线段树
这是吉如一老师在 2016 年国家队论文集上提出的线段树处理区间历史最值的问题。如果你想详细了解,可以在这里下载 2016 年的国家队论文。
此题需要你支持以下
- 区间加上
- 查询区间和、最小值和最大值。
重点在 2,3 两个操作上。先看操作 2,意思就是将区间
我们考虑像小清新线段树一样省去一些操作。记
,此时修改毫无意义,直接返回; ,此时 可以更新区间种的最大值,那么我们让和加上 ,并给挂上一个标记,将 修改为 。 ,此时我们不知道具体有多少个数会被修改,那么就继续暴力下传。
那么这种方法有多快呢?由于笔者不会势能分析,所以这里可以参考ran_qwq大佬的题解。
笔者の提交记录:
void modify_max (int node, int l, int r, int s, int t, int c) {
if (tree[node].mi >= c) return ;
if (s <= l && r <= t && tree[node].si > c) { // 这里是修改值在次大与最大之间的情况
tree[node].sum += (c - tree[node].mi) * tree[node].ci;
if (tree[node].sx == tree[node].mi) tree[node].sx = c;
if (tree[node].mx == tree[node].mi) tree[node].mx = c;
tree[node].lazi = max (tree[node].lazi, c);
tree[node].mi = tree[node].lazx = c;
return ;
}
pushdown (node, l, r);
int mid = l + ((r - l) >> 1);
if (s <= mid) modify_max (node << 1, l, mid, s, t, c);
if (t > mid) modify_max ((node << 1) + 1, mid + 1, r, s, t, c);
pushup (node);
}
先咕咕咕...
一些练习
基本操作
luogu P3372 【模板】线段树 1
luogu P3373 【模板】线段树 2
[USACO15DEC] Counting Haybale P
小清新线段树
UOJ.228(相当于花神游历各国加强版)
CodeForces 920F. SUM and REPLACE
吉司机线段树
线段树进阶
luogu P4839 P 哥的桶(线性基套线段树)
luogu P6327 区间加区间 sin 和(三角函数和差角公式,注意double)
luogu P3747 [六省联考 2017] 相逢是问候(建议先做前置,此题难度较大,建议先能灵活运用数论与线段树后再做)
SPOJ GSS7 - Can you answer these queries VII(线段树 + 树链剖分最终版,建议先做题单中的线段树 + 树链剖分和GSS1),GSS3)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具