[数据结构入门]线段树plus - 区间乘法 & 动态开点
#0.0 前置知识
本文为线段树plus,在阅读本文前,请确保你已经学会了线段树区间加法与区间查询,否则建议继续学习:
#1.0 区间乘法...?
对于区间乘法的处理本身并不麻烦,就如同处理区间加法时一样,给他一个懒标记,用的时候下传便是了,但是,问题肯定不止这么简单(不然我怎么水完这篇博客),重要的是在有其他区间操作时(比如还有区间加法)如何处理懒标记下传的顺序。所以这一节的标题应该是:
#1.1 ...是下传顺序!
思考,我们为什么要注意懒标记下传的顺序?
很简单,因为下传的顺序会影响维护的结果。
我们看下面这个修改过程:
- 假如我们有一个区间加法,又有一个区间乘法,那么我们也应该有两个懒标记,一个是
lazy_add
,维护加法的懒标记,另一个则是lazy_mul
,用于维护乘法的懒标记。 - 假如,结点 \(P\) 上同时有着加法懒标记 \(a\) 和乘法懒标记 \(b(a,b > 1)\),我们第一次操作,对区间 \([x_1,y_1]\) 进行了一次区间加法,结点 \(P\) 表示的范围 \([l,r]\) 完全包含在要修改的范围里,显然我们会直接对 \(P\) 的
lazy_add
加上要加的数 \(k\) 变为 \(a+k\), - 假如我们要对区间 \([x,y]\) 再进行一次区间加法,但结点 \(P\) 表示的范围 \([l,r]\) 一部分在修改的范围里,而 \(P\) 上又同时有着加法懒标记和乘法懒标记,根据我们已有的知识,我们显然要先下放 \(P\) 上的懒标记
- 那么,假如(
假如真TM多)- 我们先下放了
lazy_add
,那么左儿子的lazy_add
,变为了 \(a_{lson} +a+k\) - 再下放
lazy_mul
,那么,根据整体的计算,我们不仅应当对左儿子的sum
进行更改,还应该更改它的lazy_add
和lazy_mul
,修改lazy_add
时,问题出现了,现在这个lazy_add
的值是 \(a_{lson}+a+k\),这个 \(k\) 似乎不应当乘上这个懒标记诶,这个区间加上 \(k\) 应当在区间乘上这个懒标记之后操作才对,所以,先下放lazy_add
,再下放lazy_mul
的方法就这么挂了QwQ
- 我们先下放了
由上面这个足足有400个字,“假如”多到我不想打的例子可以很明显地看出,我们应当先下放lazy_mul
,再下放 lazy_add
。
同样的,直接对一个区间进行懒标记上的修改也应遵循这样的顺序
#1.2 部分代码实现
大部分代码与单纯区间加法与区间查询区别不大,处理好下传标记即可
#1.2.1 打——标——记——
inline ll len(int k){
return p[k].r - p[k].l + 1;
}
inline void update(int k,int mul,int add){
p[k].lazy_mul = (p[k].lazy_mul * mul) % mod;
p[k].lazy_add = (p[k].lazy_add * mul) % mod;
p[k].lazy_add = (p[k].lazy_add + add) % mod;
p[k].sum = (p[k].sum * mul + add * len(k)) % mod;
}
#1.2.2 下——放——标——记——
inline void pushdown(int k){
int ls = p[k].ls,rs = p[k].rs;
update(ls,p[k].lazy_mul,p[k].lazy_add);
update(rs,p[k].lazy_mul,p[k].lazy_add);
p[k].lazy_add = 0;
p[k].lazy_mul = 1; //注意乘法懒标记清空是变成 1 而不是 0.
}
#1.2.2 区——间——更——改——
下面给出区间乘法的代码,区间加法不再多说
inline void multip(int k,int l,int r,int x){
if (l <= p[k].l && p[k].r <= r){
update(k,x,0);
return;
}
pushdown(k);
int mid = (p[k].l + p[k].r) >> 1;
if (l <= mid)
multip(p[k].ls,l,r,x);
if (mid < r)
multip(p[k].rs,l,r,x);
pushup(k);
}
#1.2.3 其他
其他部分的代码与单纯区间加法区间查询没有什么两样,不再赘述
#1.3 启发
显然,这道题最重要的不是学会区间乘法,而是要学会考虑每一个过程是否会对答案造成错误的影响,要多考虑每一步步骤的合理性。而且,这样的结论及思考方式可以扩展出去,假如有三种运算怎么办?四种呢?一样分析即可。
#2.0 动态开点
#2.1 啥是动态开点?用在哪?
#2.1.1 简介
动态开点,顾名思义,这是一个随用随开点的线段树实现方式,也就是说,不再采用之前完全二叉树父子节点的2倍规则,事先不建树,用到哪个点,就建立哪个点。这样的方式,在维护的区间很大时,可以节省不少空间。
#2.1.2 用处
首先,一般维护区间最大值最小值这类的基本用不到动态开点(当然也可以用,下文会讲),因为一般空间都能接受,用到动态开点的地方一般有二:
- 维护值域(一段权值范围)而不是范围,这样的线段树也叫作权值线段树,比如维护值域中每个数出现的次数
- 可持久化线段树,当然,实现略有不同,这里不多说明
下面只简单谈谈动态开点在权值线段树中的应用。
#2.2 使用 & 代码实现
先简单说说啥是权值线段树。
举个例子,有一个数列 \(\{a_i\},(i \leq 10000,a < 10^9)\),
一般的线段树,结点代表的区间是 \(i\) 的范围,权值线段树代表的区间则是 \(a_i\) 的范围,用来维护类似 \(a_i\) 这个数出现的次数(运用了“桶”的思想),做统计用,但是 \(a_i\) 可以很大,\(i\) 却不会很大,所以一般先进行离散化,同时使用动态开点节约空间,再进行统计。权值线段树简单介绍到这里,日后会补充上权值线段树的博文。
#2.2.1 建树
动态开点时,我们并不建出一整棵树来,可能只有根结点或只有一部分,到需要修改哪里的值了,若这个结点没有被建立,再新建一个结点。很显然,这样一个父结点与左右子结点的编号就没有什么联系了,所以就需要在每个结点的空间里单独加入变量 lson
和rson
对左右儿子的编号进行储存。
新建一个结点
inline int create(){
tot ++; //tot为全局变量
p[tot].ls = p[tot].rs = p[tot].sum = 0;
return tot;
}
/*以下在主函数main()中*/
tot = 0;
root = create();
#2.2.2 进行操作(计数等)
这里以计数为例。
计数的过程,实际就像单点修改,不过因为是动态开点,左右儿子不一定已经建立,所以需要先检查左右儿子是否存在。
inline void change(int k,int l,int r,int x){
p[k].sum ++; //因为是计数,路径上的父节点总计数肯定会+1
if (l == r) //到叶结点了,上面修改过了,直接返回
return;
int mid = (l + r) >> 1;
if (mid >= x){
if (!p[k].ls) //左儿子若不存在,创建
p[k].ls = create();
change(p[k].ls,l,mid,x); //下传修改
}
else {
if (!p[k].rs) //右儿子同上
p[k].rs = create();
change(p[k].rs,mid + 1,r,x);
}
}
#2.2.3 其他
其他操作修改类似,不再过多赘述。
#2.3 在普通线段树中の使用
难道动态开点只能在权值线段树里使用吗?其实并不是,只不过也是类似静态开点,但编号的分配不再遵从二倍的关系
(其实上文里的区间乘法的代码就使用了)
#2.3.1 顺序
- 首先说明,动态开点的顺序可以有很多种,这里只说笔者本人常用的一种
- 全局变量
cnt
,根结点编号cnt
- 给根结点的左儿子一个编号
cnt + 1
,右儿子编号cnt + 2
- 以同样的方式遍历左右子树
- 找到左右儿子编号
- 不难发现,这样分配编号,一个父结点可能与他的子结点编号没有关联,所以在建树开点时,要记录该父节点的子节点的编号
#2.3.2 代码实现
这里只展示建树的不同,其他部分将原本k * 2 + 1
和k * 2 +1
分别换为 p[k].ls
与 p[k].rs
即可。
//p[k].ls为左儿子,p[k].rs是右儿子
inline void build(int x,int y,int k){
if (x == y){
p[k].sum = a[x];
p[k].l = p[k].r = x;
return;
}
p[k].l = x,p[k].r = y;
int mid = (x + y) >> 1;
p[k].ls = cnt ++;
p[k].rs = cnt ++;
build(x,mid,p[k].ls);
build(mid + 1,y,p[k].rs);
pushup(k);
}
#3.0 补充 · 权值线段树
更新日志及说明
更新
- 初次完成编辑 - \(\mathfrak{2021.2.5}\)
- 补充了 [#2.0 动态开点] 的内容 - \(\mathfrak{2021.2.6}\)
- 补充了 [权值线段树] 的学习传送门 - \(\mathfrak{2021.2.7}\)