码迷,mamicode.com
首页 > 其他好文 > 详细

初见 | 数据结构 | 线段树

时间:2021-06-24 17:45:42      阅读:0      评论:0      收藏:0      [点我收藏+]

标签:代码   前言   拓展   资料   display   type   typename   信息   数据结构   

前言

因为用来装 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}\) 的代码,其余的就都是很自然的递归修改,递归统计。

...//缺省源放在上面了
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

\[\texttt{Segment Tree!}\\texttt{Too Mach Water!} \]

在这篇完稿时,开头所说的装 Win To Go 的硬盘能用了,也算是首尾呼应罢(确信)。

参考 / 引用资料

初见 | 数据结构 | 线段树

标签:代码   前言   拓展   资料   display   type   typename   信息   数据结构   

原文地址:https://www.cnblogs.com/HRiver2/p/HR2note31.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!