算法学习笔记(43): 可持久化线段树 - 区间加!
1.算法学习笔记(∞):杂项2.算法学习笔记(1): 欧几里得算法及其扩展3.算法学习笔记(2): 欧拉定理与逆元4.算法学习笔记(3): 倍增5.算法学习笔记(3.1): ST算法6.算法学习笔记(4): 并查集及其优化7.算法学习笔记(5): 最近公共祖先(LCA)8.算法学习笔记(6): 树链剖分9.算法学习笔记(7): 二分图10.算法学习笔记(8): 网络流11.算法学习笔记(8.0): 网络流前置知识12.算法学习笔记(8.1): 网络最大流算法 EK, Dinic, ISAP13.算法学习笔记(8.2): 上下界网络流14.算法学习笔记(8.3): 网络最大流 - 模型篇15.算法学习笔记(9): 中国剩余定理(CRT)以及其扩展(EXCRT)16.算法学习笔记(10): BSGS算法及其扩展算法17.算法学习笔记(11): 原根18.算法学习笔记(12): 线性基19.算法学习笔记(13): Manacher算法20.算法学习笔记(14): 字符串哈希21.算法学习笔记(15): Trie(字典树)22.算法学习笔记(16): 组合数学基础23.算法学习笔记(17): 快速傅里叶变换(FFT)24.算法学习笔记(18): 平衡树(一)25.算法学习笔记(19): 树上启发式合并(DSU on tree)26.算法学习笔记(20): AC自动机27.算法学习笔记(21): 平衡树(二)28.算法学习笔记(22): 逆序对与原序列29.算法学习笔记(23): 马尔可夫链中的期望问题30.算法学习笔记(24): 狄利克雷卷积和莫比乌斯反演31.算法学习笔记(25): 矩阵树定理32.算法学习笔记(26): 计算几何33.算法学习笔记(27): 后缀结构34.算法学习笔记(28): 筛法35.算法学习笔记(29):分块36.算法学习笔记(30):Kruskal 重构树37.算法学习笔记(31): 李超线段树38.算法学习笔记(32): 分治39.算法学习笔记(33): 格路径与计数40.算法学习笔记(34): 矩阵乘法与线段树标记41.算法学习笔记(35): CMD Tree42.算法学习笔记(36): 期望中的停时43.算法学习笔记(37): 点分治,边分治小记44.算法学习笔记(38): 矩阵45.算法学习笔记(39): 2-SAT46.算法学习笔记(40): 具体数学47.算法学习笔记(41): 朴素多项式算法48.算法学习笔记(42): 颜色段均摊
49.算法学习笔记(43): 可持久化线段树 - 区间加!
50.算法学习笔记(44): 二维问题小计51.算法学习笔记(45): 快速沃尔什变换 FWT52.算法学习笔记(46): 离散余弦变换(DCT)可持久化线段树
也叫做主席树,单点修改时简单,使用空间 ,问题在于如何区间加。
区间覆盖可以做,但是似乎只能单点查?
所以我们需要引入标记永久化的概念。
一个标记在没有下放前会放在 个点上,这覆盖了整个操作区间。
一般来说我们会 update
更新区间 sum
标记,但是在这里,由于可持久化了,没有办法向上更新,所以我们需要在经过的每一个区间更新操作对这个区间的影响,或者说是对区间和的影响。
这只顾及了上方,还要考虑下方。
对于下方的区间我们都会有操作带来的影响,所以需要一个额外的标记记录对于下方区间的影响。
using lint = long long;
// 这里使用的是左闭右开的写法!
void add(int L, int R, lint v, int p, int l, int r) {
if (L <= l && r <= R) {
return (void)(laz[p] += v);
} int mid = (l + r) >> 1;
sum[p] += (min(R, r) - max(L, l)) * v;
if (L < mid) add(L, R, v, lc(p), l, mid);
if (R > mid) add(L, R, v, rc(p), mid, r);
}
在查询的时候,我们只需要在原本 sum
的基础上加上 laz
对于查询区间的影响即可。
值得一提的是,
laz
标记必须限制在其所对应的区间呢,它并不一定会影响整个查询的区间!
lint query(int L, int R, int p, int l, int r) {
if (L <= l && r <= R) {
return sum[p] + (r - l) * val[p];
} lint mid = (l + r) >> 1, res = (min(R, r) - max(L, l)) * val[p];
if (L < mid) res += query(L, R, lc(p), l, mid);
if (R > mid) res += query(L, R, rc(p), mid, r);
return res;
}
至于可持久化,我们需要改的东西也很少:
int clone(int p) { // 这是唯一需要多增的一个函数
laz[++use] = laz[p], sum[use] = sum[p];
lc[use] = lc[p], rc[use] = rc[p];
return use;
}
void add(int L, int R, lint v, int &p, int l, int r) {
p = clone(p); // 这是唯二需要多增的一行
if (L <= l && r <= R) {
return (void)(laz[p] += v);
} int mid = (l + r) >> 1;
sum[p] += (min(R, r) - max(L, l)) * v;
if (L < mid) add(L, R, v, lc[p], l, mid);
if (R > mid) add(L, R, v, rc[p], mid, r);
}
lint query(int L, int R, int p, int l, int r) {
if (!p) return 0; // 其实这个特判不要也罢
if (L <= l && r <= R) {
return sum[p] + (r - l) * val[p];
} lint mid = (l + r) >> 1, res = (min(R, r) - max(L, l)) * val[p];
if (L < mid) res += query(L, R, lc[p], l, mid);
if (R > mid) res += query(L, R, rc[p], mid, r);
return res;
}
接下来就是喜闻乐见的复杂度分析了,时间复杂度是 的准没错,但是空间复杂度呢?
乍一眼看上去应该是 的,因为会影响 个结点,每个结点上方有 个结点。
但是实际上还是 的,只是带了一个 的常数。
因为线段树操作区间是连续的,这使得每一层至多有两个额外的区间没有被操作,而原本我们影响到的区间有 个,所以总共会影响 个结点。
为什么有两个?因为只能是最左边和最右边的两个部分才不可以被完全覆盖,中间的是一定被覆盖了的!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?