[模版]线段树
来源:互联网 发布:微信o2o系统源码下载 编辑:程序博客网 时间:2024/05/22 06:16
(欢迎咨询QQ3228648095)
线段树
线段树是个啥呀
线段树是个毛???在前面我只能告诉你又长又骚。线段树的代码,板子题都有将近百行,稍微有点思想的题目估计就要上一百五十行了。然后差错的时候哦查到你心累,这就是线段树……
算了还是来点正能量的吧。
线段树有着不可比拟的有点,它是可以快捷地对一段数据进行在线维护、查询的数据结构,查询和维护的时间复杂度都是(log2 n)这就很舒服了。
基本原理
线段树是一种二叉搜索树,它将一个区间放在根结点里面,然后不断把区间进行分割,直到分割成一个单位区间(可以理解为一个单结点区间,或者一个不可分割的区间),然后把所有单位区间都放在叶子结点里面,我们查找的时候就可以通过树枝进行搜,如果遇到一个区间和需要查找的区间是包含关系,那就不用查找了,直接返回这个区间的相应参数,如果交叉关系,就继续分割递归,如果没有关系,那就直接return结束本层递归。
对比
和RMQ的对比
RMQ求区间最大值运用了倍增的思想,在预处理后,可以在常数时间内完成查询,也就是查询的时间复杂度是O(1),而预处理的时间复杂的是O(nlog2 n),看起来是不是比这个线段树要好很多?但是——RMQ算法是局限于离线查询,不支持更改,如果要更改的话,时间复杂度蹭蹭蹭上去,而且代码复杂度也很高,我试过,头都大了。但是线段树是支持更改的在线数据结构。
和数组直接存储的对比
数组直接存储不需要预处理,修改需要O(1)的时间,但是查找单点要O(n),查找区间的话要O(n^2)的时间。太慢了(我曹。
线段树的实现
线段树真是个好玩意儿,我们怎么实现它呢?
存储
线段树在本质上是一棵完全二叉树(想一想,为什么)
线段树的存储有很多种办法,但是最省空间的方法是:采用堆结构。对于每一个结点i,a[i]就来存储这个结点,a[2*i]来存储这个点的左孩子,a[2 *i+1]来存储这个点的右孩子。没有优化的空间复杂度是O(2n),但是数组往往需要开到4n,否则会溢出空间哦。
(待更新)
查找和更新
线段树本来就是一种二叉搜索树,查找和更新的时候就可以从根结点一层一层向下递归就ok了。
一个极好的优化
我们会发现,如果对于一棵线段树,我们每次都进行到底的搜索或修改,我们会浪费很多时间,导致tle很多点。我们会发现对于同一个元素,我们在修改维护的时候会经过一次,而且在询问的时候又会经过一次。这样对于多询问题来说就会重复判断操作。我们这里有一个解决办法——延迟修改。
我们一般称之为lazy标记(很懒,只能留到下次经过的时候再处理),然后做法就很显然了:如果当前遇到的区间被要操作的区间所覆盖,那就直接对这个根结点进行标记,以下的子结点就不用再找了,直接return掉。
各种模板
我们的模板是基于一个很好的板子题进行的:
题目
题目描述
如题,已知一个数列,你需要进行下面两种操作:
1.将某区间每一个数加上x
2.将某区间每一个数乘上x
3.求出某区间每一个数的和
输入输出格式
输入格式:
第一行包含三个整数N、M、P,分别表示该数列数字的个数、操作的总个数和模数。
第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。
接下来M行每行包含3或4个整数,表示一个操作,具体如下:
操作1: 格式:1 x y k 含义:将区间[x,y]内每个数乘上k
操作2: 格式:2 x y k 含义:将区间[x,y]内每个数加上k
操作3: 格式:3 x y 含义:输出区间[x,y]内每个数的和对P取模所得的结果
输出格式:
输出包含若干行整数,即为所有操作3的结果。
一些这道题必备的思路
简单来说,如果要修改一个区间的值,如果这个区间刚好被完全包含了就直接返回,打上标记(要改成多少),那么下次要用子节点的时候将标记下传即可,显然,对于本题要打两个tag,那么sum[o] = sum[o] * mul[o] + add[o],sum为和,mul为乘的数,add为加的数,那么如果我们乘一个数c,sum[o] = sum[o] * mul[o] * c + add[o] * c,将mul[o] * c和add[o] * c看作两个整体,那么可以发现如果乘一个数c的话,add数组要*c,mul数组也要*c,同理,如果加一个数c,那么只需要add数组+c即可,因为在下传标记的时候既有加,又有减(可能为0),所以add和mul数组的计算一定要将这两种情况都计算到.如果在线段树上几个区间操作的性质相同的,先把要求的数用操作需要的量给表示出来,然后对于每一种操作看看需要维护的标记的变化,最后合并一下即可.
声明
在上模板代码之前,我们先用了这个头文件:
#include<bits/stdc++.h>
这两个宏定义:
#define le l,mid,o*2#define re mid+1,r,o*2+1
n用来存储原数列的个数,m用来存储操作的个数,p用来存储需要mod的数,a数组用来存原数列,add数组用来存加法的lazy标记,mul用来存乘法的lazy标记,sum用来存累加和(或对p取模后的值);x和y代表需要更改的区间的两个端点。
建树
void build(long long l,long long r,long long o){ mul[o]=1;//乘法基数1 add[o]=0;//加法基数0 if(l==r)//如果这个区间两端点重合啦,就是锁定单点了,这个区间只有一个元素 { sum[o]=a[l];//这个区间的总和就是元素自身了 return; } int mid=(l+r)>>1;//获取中间点 build(le);//继续造树 build(re);//十年树木百年树人 sum[o]=(sum[o*2]+sum[o*2+1])%p;//第o个点的累加和是两个孩子的累加和之和}
传递lazy标记
void pushdown(int o,int k){ sum[o*2]=(sum[o*2]*mul[o]+add[o]*(k-(k>>1)))%p;//左子树的累加和更新 sum[o*2+1]=(sum[o*2+1]*mul[o]+add[o]*(k>>1))%p;//右子树的累加和更新 //更新的顺序:先乘法标记的更新,再加上累加标记的更新。并且累加标记的更新还要注意累加的次数,一个是(k-(k>>1)),另一个是(k>>1)。 mul[o*2]=mul[o*2]*mul[o]%p mul[o*2+1]=mul[o*2+1]*mul[o]%p; //乘法标记也要更新的。千万别忘记 add[o*2]=(add[o*2]*mul[o]+add[o])%p; add[o*2+1]=(add[o*2+1]*mul[o]+add[o])%p; //加法标记更新的时候要先考虑乘法标记的更新。 mul[o]=1; add[o]=0; //标记清空。}
区间自加
void jia(int l,int r,int o)//三个参数,l和r代表当前在查找的区间的两个端点,o代表当前查找的区间的根结点{ if(x<=l&&r<=y)//如果区间完全被覆盖 { add[o]=(add[o]+v)%p;//对lazy标记直接累加v,并且模p(一步一模可以避免最后long long溢出的问题) sum[o]=(sum[o]+v*(r-l+1))%p;//对于该点的累加和也要相应加上每一个单点的改变量 return;//lazy标记的初衷,直接退出 } pushdown(o,r-l+1);//传递lazy标记 int mid=(l+r)>>1;//获取区间中点 if(x<=mid) jia(le);//如果x在中点左边,就说明有必要搜索左子树 if(y>mid) jia(re);//如果y在中点右边,就说明有必要搜索右子树 sum[o]=(sum[o*2]+sum[o*2+1])%p;//是时候更新根结点的累加和了。
区间乘法
void cheng(int l,int r,int o){ if(x<=l&&r<=y) //如果区间完全包含 { add[o]=(add[o]*v)%p; mul[o]=(mul[o]*v)%p; sum[o]=(sum[o]*v)%p; //累加和、乘法标记、加法标记全部更新。 return;//更新完就直接返回,不用多说话。 } pushdown(o,r-l+1);//传递标记 int mid=(l+r)>>1; if(x<=mid) cheng(le); if(y>mid) cheng(re); sum[o]=(sum[o*2]+sum[o*2+1])%p; //这一段同上文加法。}
区间查找
long long query(int l,int r,int o){ if(x<=l&&r<=y) return sum[o]%p; //如果完全包含,直接返回值(lazy标记已经被pushdown了) int mid=(l+r)>>1;//取中点 pushdown(o,r-l+1);//传递标记 long long temp=0; if(x<=mid) temp=(temp+query(le))%p; //左子树的累加和 if(y>mid) temp=(temp+query(re))%p; //右子树的累加和 sum[o]=(sum[o*2]+sum[o*2+1])%p; //更新根结点 return temp%p; //返回最后值}
主函数的一些边缘操作
int main(){ n=read(); m=read(); p=read(); //快读(见下) for(int i=1;i<=n;i++) a[i]=read(); //读入数据 build(1,n,1); //构造树 for(int i=1;i<=m;i++) { pattern=read(); //读入操作类型 if(pattern==1) { x=read(); y=read(); v=read(); cheng(1,n,1); }//1就是乘法 if(pattern==2) { x=read(); y=read(); v=read(); jia(1,n,1); }//2就是加法 if(pattern==3) { x=read(); y=read(); printf("%lld\n",query(1,n,1)%p); }//3就是查找区间累加和并取模 } return 0;}
快读
这是个好技能。希望大家get到
inline long long read(){ long long num=0; char c=getchar(); for(;c<'0'||c>'9';c=getchar()); for(;c>='0'&&c<='9';c=getchar())num=num*10+c-'0'; return num;}
- 线段树 模版
- 线段树模版
- 线段树模版
- 二维线段树模版
- 刘汝佳--线段树模版
- 线段树模版
- 线段树模版
- 线段树模版
- 线段树模版
- 线段树模版
- 【笔记+模版】 线段树
- [模版]线段树
- 线段树模版和学习
- 线段树模版(转)
- 线段树 区间更新 模版
- 模版(自建线段树)
- 洛谷p3372 线段树模版
- 线段树知识点模版详解
- 第四周项目3---单链表应用
- Windows下安装Caffe简易办法
- 素数与完全数
- 论文笔记:Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling
- 修改网站图标
- [模版]线段树
- Java DateApi
- 继承与多态经典题目注释解析
- java的学习笔记——一个城堡小游戏
- Linux小工具的应用,grep,sort,wc,cut
- Java 求两个日期之间天数(DateApi)
- Java常用算法之堆排序
- selenium 问题定期总结
- 7种数据库事务传递性代码实例详解