初见 | 数据结构 | 线段树
前言
因为用来装 Win To Go 的硬盘炸了,所以今天下午就水一个博客罢。
下面是目录:
引入
首先我们需要知道线段树是用来解决什么问题的数据结构。
先看她的名字来进行大胆的猜测:
线段树,顾名思义,是和线段有关的树,那么其实线段树就是一种维护区间信息的数据结构。
基本结构分析
和树状数组的基本结构类似,线段树也是把一个序列分为各种大小的区间,再按照相应的包含关系构成一棵树。
说的不是很清楚?上图看看。
我们可以清晰的看到最小的节点是长度为 1 的区间,她的父节点都是包含她的更大的区间。显然的是对于长度为 \(n\) 的区间,节点总个数为 \(2n\)。
在传统递归建树下,我们建树的复杂度是 \(O(n\log n)\) 的。
这里是堆式储存,我们要开 \(4n\) 的数组,至于缘由详见这一篇文章:线段树为什么要开4倍空间 - 拾月凄辰 - 博客园 (cnblogs.com)。
如果您不想去看,我在这里大约简述一下:因为我们建树的时候建出来的不一定是一棵满二叉树,甚至都不是一颗完全二叉树,因此 \(2n\) 的数组空间肯定不够用。实际上我们最大节点的编号是在 \(3n\) 和 \(4n\) 之间的,因此我们开 \(4n\) 的数据才能保证不会发生越界。
同时,还有一种建树的方式叫做动态开点。理论上动态开点的时间复杂度也是 \(O(n\log n)\) 的,只是动态开点能够节省数组空间至 \(2n\)。
下面我来结合代码讲解如何实现线段树的基本结构。
实现
我先会对每一个操作进行实现,然后再给出全部程序。
这里使用的莉题是 浴谷 P3373 【模板】线段树 2 。
缺省源
因为我的缺省源很长,所以为了节约篇幅,就先把缺省源放在这里。
#define Heriko return
#define Deltana 0
#define S signed
#define LL long long
#define R register
#define I inline
#define lc(x) (x<<1)
#define rc(x) (x<<1|1)
#define CI const int
#define mst(a, b) memset(a, b, sizeof(a))
using namespace std;
template<typename T>
I void fr(T &x) {...//快读}
template<typename T>
I void fw(T x,bool k) {...//快输}
建树
要致富,先建树。
线段树,先建树。
为了方便,这里我是用的结构体来记录节点,\(\texttt{Pushup}\) 函数是合并信息。
struct node
{
LL val,add,mul;
}
t[MXX];
I void pushup(LL rt)
{
t[rt].val=(t[rc(rt)].val+t[lc(rt)].val)%mod;
}
由于我本篇的剩余代码基本都是按照传统的递归建树写的,因此这里的建树的方法就是传统递归建树。
传统递归建树 Code
void build(LL rt,LL l,LL r)
{
t[rt].add=0;t[rt].mul=1;//区间加tag 和 区间乘tag
if(l==r) {fr(t[rt].val);Heriko;}
LL mid=(l+r)>>1;
build(lc(rt),l,mid);
build(rc(rt),mid+1,r);
pushup(rt);
}
至于动态开点,可以自行 BDFS。这里提供一个我觉得讲的还可以的:算法学习笔记(49): 线段树的拓展 - 知乎 (zhihu.com)。
简单解释一下上面的代码。先是把 tag 清零,然后进行二分继续建树,最后上传区间值。
下传标记
考虑到直接对区间内的每一个数进行加法是非常慢的,所以我们用到一个叫做 \(\texttt{Lazy Tag}\) 的东西来优化时间复杂度。当我们要查询 / 进行操作的时候再把 Tag 下传,这样在每次查询 / 修改之前,每个节点的值都已经及时的更新完毕。
因为这道题是有区间加法和区间乘法两个操作,所以我们要维护两个 Tag。
要注意的是要先处理乘法 Tag 再处理加法 Tag。
标记下传 Code
I void pushdown(LL rt,LL l)
{
t[lc(rt)].val=(t[lc(rt)].val*t[rt].mul+t[rt].add*(l-(l>>1)))%mod;
t[rc(rt)].val=(t[rc(rt)].val*t[rt].mul+t[rt].add*(l>>1))%mod;
(t[lc(rt)].mul*=t[rt].mul)%=mod;
(t[rc(rt)].mul*=t[rt].mul)%=mod;
t[lc(rt)].add=(t[lc(rt)].add*t[rt].mul+t[rt].add)%mod;
t[rc(rt)].add=(t[rc(rt)].add*t[rt].mul+t[rt].add)%mod;
t[rt].add=0;t[rt].mul=1;
}
区间修改
区间乘
实际上就是打上标记然后二分递归完事~
Code
void mul(LL rt,LL l,LL r,LL x,LL y,LL val)
{
if(x<=l and r<=y)
{
(t[rt].val*=val)%=mod;
(t[rt].mul*=val)%=mod;
(t[rt].add*=val)%=mod;
Heriko;
}
pushdown(rt,r-l+1);
LL mid=(l+r)>>1;
if(x<=mid) mul(lc(rt),l,mid,x,y,val);
if(y>mid) mul(rc(rt),mid+1,r,x,y,val);
pushup(rt);
}
要注意在继续递归前先 \(\texttt{Pushdown}\),以及在递归完成后 \(\texttt{Pushup}\),否则你会像我一样调亿年。
区间加
实际上区间加的操作和区间乘基本上是一致的。
Code
void add(LL rt,LL l,LL r,LL x,LL y,LL val)
{
if(x<=l and r<=y)
{
(t[rt].add+=val)%=mod;
(t[rt].val+=val*(r-l+1))%=mod;
Heriko;
}
pushdown(rt,r-l+1);
LL mid=(l+r)>>1;
if(x<=mid) add(lc(rt),l,mid,x,y,val);
if(y>mid) add(rc(rt),mid+1,r,x,y,val);
pushup(rt);
}
查询
因为是查询值所以我们一样去递归的累加子树值即可。
LL query(LL rt,LL l,LL r,LL x,LL y)
{
if(x<=l and r<=y) Heriko t[rt].val%mod;
pushdown(rt,r-l+1);
LL mid=(l+r)>>1;
LL ans=0;
if(x<=mid) ans+=query(lc(rt),l,mid,x,y)%mod;
if(y>mid) ans+=query(rc(rt),mid+1,r,x,y)%mod;
Heriko ans%mod;
}
全部代码
最后就是全部代码,实际上如果整体来看,线段树变化最大的地方就是 \(\texttt{Pushdown}\) 的代码,因为你用线段树维护的东西不一样,你写的 \(\texttt{Pushdown}\) 都会不一样。
因此,应当仔细理解代码以保证使用线段树维护不同数据的时候都能写出正确的 \(\texttt{Pushdown}\)。(感谢大佬 \(\texttt{Suzt_ilymtics}\) 的评论)
至于剩下的部分基本上都是比较固定的递归形式,只要能够理解自己要维护的是什么,要修改的是什么,基本都能比较自然地写出来。
...//缺省源放在上面了
CI MXX=1e6+5;
LL n,m,mod;
struct node
{
LL val,add,mul;
}
t[MXX];
I void pushup(LL rt)
{
t[rt].val=(t[rc(rt)].val+t[lc(rt)].val)%mod;
}
void build(LL rt,LL l,LL r)
{
t[rt].add=0;t[rt].mul=1;
if(l==r) {fr(t[rt].val);Heriko;}
LL mid=(l+r)>>1;
build(lc(rt),l,mid);
build(rc(rt),mid+1,r);
pushup(rt);
}
I void pushdown(LL rt,LL l)
{
t[lc(rt)].val=(t[lc(rt)].val*t[rt].mul+t[rt].add*(l-(l>>1)))%mod;
t[rc(rt)].val=(t[rc(rt)].val*t[rt].mul+t[rt].add*(l>>1))%mod;
(t[lc(rt)].mul*=t[rt].mul)%=mod;
(t[rc(rt)].mul*=t[rt].mul)%=mod;
t[lc(rt)].add=(t[lc(rt)].add*t[rt].mul+t[rt].add)%mod;
t[rc(rt)].add=(t[rc(rt)].add*t[rt].mul+t[rt].add)%mod;
t[rt].add=0;t[rt].mul=1;
}
void mul(LL rt,LL l,LL r,LL x,LL y,LL val)
{
if(x<=l and r<=y)
{
(t[rt].val*=val)%=mod;
(t[rt].mul*=val)%=mod;
(t[rt].add*=val)%=mod;
Heriko;
}
pushdown(rt,r-l+1);
LL mid=(l+r)>>1;
if(x<=mid) mul(lc(rt),l,mid,x,y,val);
if(y>mid) mul(rc(rt),mid+1,r,x,y,val);
pushup(rt);
}
void add(LL rt,LL l,LL r,LL x,LL y,LL val)
{
if(x<=l and r<=y)
{
(t[rt].add+=val)%=mod;
(t[rt].val+=val*(r-l+1))%=mod;
Heriko;
}
pushdown(rt,r-l+1);
LL mid=(l+r)>>1;
if(x<=mid) add(lc(rt),l,mid,x,y,val);
if(y>mid) add(rc(rt),mid+1,r,x,y,val);
pushup(rt);
}
LL query(LL rt,LL l,LL r,LL x,LL y)
{
if(x<=l and r<=y) Heriko t[rt].val%mod;
pushdown(rt,r-l+1);
LL mid=(l+r)>>1;
LL ans=0;
if(x<=mid) ans+=query(lc(rt),l,mid,x,y)%mod;
if(y>mid) ans+=query(rc(rt),mid+1,r,x,y)%mod;
Heriko ans%mod;
}
LL x,l,r,val;
S main()
{
fr(n),fr(m),fr(mod);
build(1,1,n);
while(m--)
{
fr(x);
if(x==1) {fr(l),fr(r),fr(val);mul(1,1,n,l,r,val);}
else if(x==2) {fr(l),fr(r),fr(val);add(1,1,n,l,r,val);}
else {fr(l),fr(r);fw(query(1,1,n,l,r)%mod);}
}
Heriko Deltana;
}
End
因为本人写这篇的时候比较懒,很多地方都没有仔细的去写,也可能会产生一定的错误。
因此,您若是发现了任何错误,可以在评论区纠正,感谢各位。
在这篇完稿时,开头所说的装 Win To Go 的硬盘能用了,也算是首尾呼应罢(确信)。
参考 / 引用资料
- [1] 线段树 —— OI Wiki
- [2] [数据结构入门]线段树 —— Dfkuaid
- [3] 线段树为什么要开4倍空间 —— 拾月凄辰
- [4] 算法学习笔记(49): 线段树的拓展 - 知乎 (zhihu.com) —— Pecco