欢迎光临 ~|

Laijinyi

园龄:1年7个月粉丝:2关注:2

📂总结
🔖算法
2024-04-20 21:01阅读: 113评论: 0推荐: 0

标记永久化

也就是没有 push_down

注意前面两个问题是具有启发性的,不要略去不看

区间加,单点和

每个节点 u 额外维护一个 add 值,表示 u 所代表的区间的总增加量

此时只考虑该区间的增加情况,不用考虑它祖先或者儿子的增加情况

这样做的代价是查询时把没考虑的祖先 以及 儿子 的增加情况考虑进去

区间加:把 logn 个点的 add 加一下

单点和:对于一个单点,所有包含它的区间的 add 都对他有贡献

于是把经过的节点的 add 累加起来回答

区间加,区间和

对当前区间 u,可以想到维护两个信息:

  • u 的区间和 sum(u)
  • u 的增加量 add(u)

但是这两个标记是如何具体地维护从而支持区间加、区间和的呢?我们不知道

注意首先要想求出 u 的答案,我们需要考虑除了 u 本身被修改的影响之外的两个影响:

  • u 的儿子对 u 答案的影响
  • u 的祖先对 u 答案的影响

容易想到让 u 的标记维护第一个影响,然后在查询的时候对答案考虑第二个影响(查询时路过的节点都是当前查询节点的祖先,而 push_up 就相当于用儿子更新 u

而由于询问时对于一个祖先需要方便地算出它对它的子树的影响,于是我们令 add(u) 表示 u 整个区间所有数都同步累加的增加量

此时 query 的祖先的影响就容易维护了,而 update 对该标记的影响也就是对所操作区间的 add 累加.

而由于节点内部需要记录所有儿子对 u 的影响,所以令 sum(u) 为考虑 u 子树内所有儿子对 u 影响的区间和

此时对于 sum 有两种维护方法:

  • 一种方法:

    考虑通过 push_up 更新儿子对 u 的影响

    假设此时儿子的信息都已经搞定,也就是儿子的 sum 已经考虑了儿子及其子树的修改情况,现在我们需要用儿子的信息更新 u 的信息

    直接 sum(u)=sum(lc)+sum(rc) 即可

    而对于当前修改节点,修改 sum 即可

  • 一种方法:

    update 时顺带把路过的节点的 sum 更新了,因为路过的节点一定是当前修改的节点的祖先,这次 update 一定影响到了这些点的区间和

注意虽然这两种方法的思想不同,一种是在回溯时更新,一种是在递归过程中更新,但是代码差异很小,同时这里建议写 push_up

然后在 query 时直接用 sum(u) 即可

所以 sum 这个标记是只考虑 u 这个单独的区间的内部情况的,也就是只考虑了 u 以及 其子树所有的修改情况,没有考虑其祖先的修改对它的影响

于是我们就推导出了标记永久化的过程.

总结:

注意这个问题中区间 u 维护了如下两个信息:

  • 考虑 u 子树内的所有修改(增加)情况,u 的区间和 sum(u)
  • 考虑 u 作为祖先,它的修改(增加)对儿子的答案的贡献 add(u)

更形象地,sum(u) 的范围是 u 的整棵子树,而 add(u) 的范围是 u 这一个节点,对儿子的答案的贡献

这种标记维护方式就是标记永久化的核心思想.

而这个方法的必要性和充分性都在前面的一步步推导中体现了.

模板题代码
#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

区间赋值、单点查询

假设前面两节你都看懂了

发现从上往下合并标记的时候有困难:如何合并?

发现只要保留最晚的赋值标记就可以

于是再维护一个时间戳,表示这个赋值标记的时间,合并时保留最晚的标记

区间加、区间赋值、区间和

维护 sum,add,tag,timeaddtag 任意时刻总是保留其一

区间加:若有 tagtagtag+x,timenow;否则 addadd+x

区间赋值:tagx,timenow

区间和:合并标记时 addtag 保留其一

  • tag:若当前节点有 tag 保留最晚的 tag,否则 tagtag+addu
  • tag:若当前节点有 tagtagtagu+add,add0,否则 addadd+addu

算答案时类似

区间整除、区间加、区间最值

标记 (a,b) 表示先加 a 再除以 b,记 为标记合并

区间加:x+ab+p=x+a+bpb

区间整除:x+abp=x+abp

这就说明了 (a,b)(c,d)=(a+bc,bd),没有交换律

网上有个说法是若能找到一个 a 使得 ab=ba,就可以做

具体是强制让儿子的标记比父亲的标记晚

但是这样不可行,复杂度是错的

于是这道题可以普通线段树做

区间加、区间和、区间和历史最大值

标记 (a,b,c) 表示该区间加了 a,区间和为 b,区间和历史最大值为 c

区间加:(a,b,c)(a+p,b+(rl+1)p,max(c,b+(rl+1)p))

这是可以标记永久化的

区间 checkmax、区间 max、单点查

标记 (a,b) 表示 checkmax 了 a,当前区间 max=b

区间 checkmax:(a,b)(max(a,x),max(b,x))

这也可以方便的标记永久化

区间加、区间乘、区间和

标记 (a,b,c) 表示加、乘、和,钦定先乘再加(即 x 的真实值为 ax+b

区间加:(a,b,c)(a+x,b,c+x)

区间乘:(a,b,c)(ax,bx,cx)

所以 (a,b,c)(d,e)=(ae+d,be,ce+d)

可以普通线段树做


不具有交换律的标记如何维护?

容易想到:对每个标记维护时间戳

从根节点走到当前节点一共经过了 logn 个标记

用 set 维护从根节点到当前点的所有标记,是按时间从早到晚排序

到达相应节点后再把 set 上的标记一个个合并

时间复杂度 O(nlog2n)

因为它就是普通线段树区间查询再乘了个 set 的 log

而且实际上遍历 set 是 O(n) 的,n 为 set 内元素个数

但是这个想法是错的,具体读者自证

本文作者:Laijinyi

本文链接:https://www.cnblogs.com/laijinyi/p/18148162

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Laijinyi  阅读(113)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起