[数据结构入门]线段树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_addlazy_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 建树

动态开点时,我们并不建出一整棵树来,可能只有根结点或只有一部分,到需要修改哪里的值了,若这个结点没有被建立,再新建一个结点。很显然,这样一个父结点与左右子结点的编号就没有什么联系了,所以就需要在每个结点的空间里单独加入变量 lsonrson 对左右儿子的编号进行储存。

新建一个结点

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 + 1k * 2 +1 分别换为 p[k].lsp[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}\)

个人主页

欢迎到以下地址支持作者!
Github戳这里
Bilibili戳这里
Luogu戳这里

posted @ 2021-02-05 21:52  Dfkuaid  阅读(163)  评论(0编辑  收藏  举报