标记永久化
也就是没有 push_down
注意前面两个问题是具有启发性的,不要略去不看
区间加,单点和
每个节点 额外维护一个 add 值,表示 所代表的区间的总增加量
此时只考虑该区间的增加情况,不用考虑它祖先或者儿子的增加情况
这样做的代价是查询时把没考虑的祖先 以及 儿子 的增加情况考虑进去
区间加:把 个点的 add 加一下
单点和:对于一个单点,所有包含它的区间的 add 都对他有贡献
于是把经过的节点的 add 累加起来回答
区间加,区间和
对当前区间 ,可以想到维护两个信息:
- 的区间和
- 的增加量
但是这两个标记是如何具体地维护从而支持区间加、区间和的呢?我们不知道
注意首先要想求出 的答案,我们需要考虑除了 本身被修改的影响之外的两个影响:
- 的儿子对 答案的影响
- 的祖先对 答案的影响
容易想到让 的标记维护第一个影响,然后在查询的时候对答案考虑第二个影响(查询时路过的节点都是当前查询节点的祖先,而 push_up 就相当于用儿子更新 )
而由于询问时对于一个祖先需要方便地算出它对它的子树的影响,于是我们令 表示 整个区间所有数都同步累加的增加量
此时 query 的祖先的影响就容易维护了,而 update 对该标记的影响也就是对所操作区间的 累加.
而由于节点内部需要记录所有儿子对 的影响,所以令 为考虑 子树内所有儿子对 影响的区间和
此时对于 有两种维护方法:
-
一种方法:
考虑通过 push_up 更新儿子对 的影响
假设此时儿子的信息都已经搞定,也就是儿子的 已经考虑了儿子及其子树的修改情况,现在我们需要用儿子的信息更新 的信息
直接 即可
而对于当前修改节点,修改 即可
-
一种方法:
update 时顺带把路过的节点的 更新了,因为路过的节点一定是当前修改的节点的祖先,这次 update 一定影响到了这些点的区间和
注意虽然这两种方法的思想不同,一种是在回溯时更新,一种是在递归过程中更新,但是代码差异很小,同时这里建议写 push_up
然后在 query 时直接用 即可
所以 这个标记是只考虑 这个单独的区间的内部情况的,也就是只考虑了 以及 其子树所有的修改情况,没有考虑其祖先的修改对它的影响
于是我们就推导出了标记永久化的过程.
总结:
注意这个问题中区间 维护了如下两个信息:
- 考虑 子树内的所有修改(增加)情况, 的区间和
- 考虑 作为祖先,它的修改(增加)对儿子的答案的贡献
更形象地, 的范围是 的整棵子树,而 的范围是 这一个节点,对儿子的答案的贡献
这种标记维护方式就是标记永久化的核心思想.
而这个方法的必要性和充分性都在前面的一步步推导中体现了.
模板题代码
#include <bits/stdc++.h> using namespace std; typedef long long ll; constexpr int N = 1e5 + 10; ll sum[N << 2], add[N << 2]; int n, m; #define lc (u << 1) #define rc ((u << 1) | 1) #define mid ((l + r) >> 1) void Up(int u, int l, int r) { sum[u] = sum[lc] + sum[rc] + 1ll * (r - l + 1) * add[u]; } void build(int u, int l, int r) { if (l == r) return cin >> sum[u], void(); build(lc, l, mid), build(rc, mid + 1, r), Up(u, l, r); } void Upd(int u, int l, int r, int x, int y, ll v) { if (y < l || r < x) return; if (x <= l && r <= y) return add[u] += v, sum[u] += 1ll * (r - l + 1) * v, void(); Upd(lc, l, mid, x, y, v), Upd(rc, mid + 1, r, x, y, v), Up(u, l, r); } ll Qry(int u, int l, int r, int x, int y) { if (y < l || r < x) return 0ll; if (x <= l && r <= y) return sum[u]; return Qry(lc, l, mid, x, y) + Qry(rc, mid + 1, r, x, y) + 1ll * (min(y, r) - max(x, l) + 1) * add[u]; } int main() { ios::sync_with_stdio(false), cin.tie(nullptr); cin >> n >> m, build(1, 1, n); while (m--) { int op; cin >> op; if (op == 2) { int l, r; cin >> l >> r; cout << Qry(1, 1, n, l, r) << "\n"; } else { int l, r; ll c; cin >> l >> r >> c; Upd(1, 1, n, l, r, c); } } }
修改时改一路上的 sum、完全包含的 tag
查询时加一路上的 tag、完全包含的 sum
区间赋值、单点查询
假设前面两节你都看懂了
发现从上往下合并标记的时候有困难:如何合并?
发现只要保留最晚的赋值标记就可以
于是再维护一个时间戳,表示这个赋值标记的时间,合并时保留最晚的标记
区间加、区间赋值、区间和
维护 , 和 任意时刻总是保留其一
区间加:若有 ,;否则
区间赋值:
区间和:合并标记时 和 保留其一
- 有 :若当前节点有 保留最晚的 ,否则
- 无 :若当前节点有 则 ,否则
算答案时类似
区间整除、区间加、区间最值
标记 表示先加 再除以 ,记 为标记合并
区间加:
区间整除:
这就说明了 ,没有交换律
网上有个说法是若能找到一个 使得 ,就可以做
具体是强制让儿子的标记比父亲的标记晚
但是这样不可行,复杂度是错的
于是这道题可以普通线段树做
区间加、区间和、区间和历史最大值
标记 表示该区间加了 ,区间和为 ,区间和历史最大值为
区间加:
这是可以标记永久化的
区间 checkmax、区间 max、单点查
标记 表示 checkmax 了 ,当前区间
区间 checkmax:
这也可以方便的标记永久化
区间加、区间乘、区间和
标记 表示加、乘、和,钦定先乘再加(即 的真实值为 )
区间加:
区间乘:
所以
可以普通线段树做
不具有交换律的标记如何维护?
容易想到:对每个标记维护时间戳
从根节点走到当前节点一共经过了 个标记
用 set 维护从根节点到当前点的所有标记,是按时间从早到晚排序
到达相应节点后再把 set 上的标记一个个合并
时间复杂度
因为它就是普通线段树区间查询再乘了个 set 的 log
而且实际上遍历 set 是 的, 为 set 内元素个数
但是这个想法是错的,具体读者自证
本文作者:Laijinyi
本文链接:https://www.cnblogs.com/laijinyi/p/18148162
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步