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

线段树

时间:2019-02-05 20:54:38      阅读:214      评论:0      收藏:0      [点我收藏+]

标签:决定   数列   com   info   心得   个人   区间   父节点   相同   

线段树是个好东西

这篇文章主要是讲一讲

基础,自己的心得;

好,那就开始!

一.定义

线段树(Segment Tree)是一棵

二叉搜索树
树上每个节点代表一个区间[ l , r ]
同一层的节点所代表的区间,相互不会重叠

同一层节点所代表的区间,加起来是个连续的区间

对于每一个非叶结点所表示的结点 [ l , r ],

其左儿子表示的区间为 [ l , ( l + r ) / 2 ] ([ l , ( l + r ) >>1 ])

右儿子表示的区间为 [ ( l + r ) / 2 + 1, r ] (除法去尾取整) ( [ ( l + r ) >> 1 + 1 , r ] )

叶子节点表示的区间长度为1

 

ps: a * 2 = a << 1 ;

a * 2 + 1 = a << 1 | 1 ;
(下文 全部用位运算替代)

#define ll long long
struct node{
ll l,r,sum,add,mul;
}t[M<<2];

M为数列元素数 (M<<2=M*4),l是左端点,r是右端点,sum为数列区间[ l , r ]中所有元素总和,add为加法标记,mul为乘法标记(与lazy是一个意思,不懂的后面会讲)

开四倍空间证明

证明:
设共有N个节点,则有

ceil(log2(n))层,

所以共需要2^(ceil(log2(n))+1)-1节点

技术图片

 

 个人理解:其实就是将一个数列不断二分,直到区间只有一个叶子节点。而每个节点的父节点具有在一定情况下能代表其所有子节点的性质(比如求和时,t[a].sum=t[a<<1].sum+t[a<<1|1].sum),从而通过访问父节点,得到性质,节省访问子节点的时间,降低时间复杂度

如图

技术图片

二.操作

  1.建树

    void build(根节点(一般是1),左端点,右端点)

    先二分区间(层层递归),直至区间左端点等于右端点,这意味着到达叶子节点,所以读取数据;

     最后递归回去,求得区间的sum值。

void build(ll tr,ll l,ll r)
{
    t[tr].l=l,t[tr].r=r,t[tr].mul=1;
    if(l==r)
    {
        t[tr].sum=g();  //inline ll g() 快读
        return ;
    }
    R ll mid=(l+r)>>1;
    build(tr<<1,l,mid);
    build(tr<<1|1,mid+1,r);
    t[tr].sum=t[tr<<1].sum+t[tr<<1|1].sum;
}

 

 

 

  2.改变元素的值

    (1)加法

      void add(根节点,左端点,右端点,增量)

      在现有区间(这是由根节点决定的->相当于通过根确定子树->从而确定区间),二分区间位置  

      如果递归时,现有区间就是所要更改的区间,那么直接更改整个区间的sum,使sum+=inc*(r-l+1)(就是增量*区间长度),同时add+=inc(不能是add=inc,因为add可能不为0),add表示的是[l,r]区间中所有的叶子节点都少加了add,而之所以要有这个add是为了减少不必要的操作(此时没必要把每个子节点的值都修改)(其实原因就是上文:每个节点的父节点具有在一定情况下能代表其所有子节点的性质,从而通过访问父节点,得到性质,节省访问子节点的时间,降低时间复杂度)

            而所要更改的区间的左端点如果大于现有区间的mid,则递归右半区间;

            而所要更改的区间的右端点如果小于现有区间的mid,则递归左半区间;

            而所要更改的区间就在现有区间内的的话,分别递归区间[l,mid]和[mid+1,r];

            细心的你(?)可能发现,为什么在后三种情况递归前,会有一些add(),和mul()运算?

            因为我们记得 “每个节点的父节点具有在一定情况下能代表其所有子节点的性质” 对吧? 我们也知道,父节点的sum是由子节点递归而来的吧? 所以如果不下放add(此时这个操作是必要的),那么父节点的sum不能被更新,于是就不能代表子节点的性质了

如图:技术图片

所以实际上是为了保持父节点的代表性。。

 

void add(ll tr,ll l,ll r,ll inc)
{
if(t[tr].l==l&&t[tr].r==r) { t[tr].sum+=inc*(r-l+1); t[tr].add+=inc; return ; } R ll mid=(t[tr].l+t[tr].r)>>1; if(t[tr].mul!=1) mul(tr<<1,t[tr].l,mid,t[tr].mul),mul(tr<<1|1,mid+1,t[tr].r,t[tr].mul),t[tr].mul=1; if(t[tr].add!=0) add(tr<<1,t[tr].l,mid,t[tr].add),add(tr<<1|1,mid+1,t[tr].r,t[tr].add),t[tr].add=0; if(l>mid) add(tr<<1|1,l,r,inc); else if(r<mid+1) add(tr<<1,l,r,inc); else add(tr<<1,l,mid,inc),add(tr<<1|1,mid+1,r,inc); t[tr].sum=t[tr<<1].sum+t[tr<<1|1].sum; }

 

 

    (2)乘法

      void mul(根节点,左端点,右端点,增量)

      你会发现,递归的过程基本相同;

        那我需要解释一下,为什么先乘后加

        若对[l,r]进行+1,*2,+1的操作

        则实际上是(a+1)*2 + 1=2*a+2 + 1;

        所以mul要sum*=inc,mul*=inc,add*=inc

        所以先前的add要乘mul,在进行add

        

void mul(ll tr,ll l,ll r,ll inc)
{
    if(t[tr].l==l&&t[tr].r==r)
    {
        t[tr].sum*=inc;
        t[tr].mul*=inc;
        t[tr].add*=inc;
        return ;
    }
    R ll mid=(t[tr].l+t[tr].r)>>1;
    if(t[tr].mul!=1) 
        mul(tr<<1,t[tr].l,mid,t[tr].mul),mul(tr<<1|1,mid+1,t[tr].r,t[tr].mul),t[tr].mul=1;
    if(t[tr].add!=0)
        add(tr<<1,t[tr].l,mid,t[tr].add),add(tr<<1|1,mid+1,t[tr].r,t[tr].add),t[tr].add=0;
    if(l>mid) mul(tr<<1|1,l,r,inc);
    else if(r<mid+1) mul(tr<<1,l,r,inc);
    else mul(tr<<1,l,mid,inc),mul(tr<<1|1,mid+1,r,inc);
    t[tr].sum=t[tr<<1].sum+t[tr<<1|1].sum;
}

 

   

 

   3.求和

 

     ll query(根节点,左端点,右端点)

    你又会发现,递归的过程同add

     所以理解后就很好懂啦。

ll query(ll tr,ll l,ll r)
{
    if(t[tr].l==l&&t[tr].r==r) return t[tr].sum;
    R ll mid=(t[tr].l+t[tr].r)>>1;
    if(t[tr].mul!=1) 
        mul(tr<<1,t[tr].l,mid,t[tr].mul),mul(tr<<1|1,mid+1,t[tr].r,t[tr].mul),t[tr].mul=1;
    if(t[tr].add!=0)
        add(tr<<1,t[tr].l,mid,t[tr].add),add(tr<<1|1,mid+1,t[tr].r,t[tr].add),t[tr].add=0;
    if(l>mid) return query(tr<<1|1,l,r);
    else if(r<mid+1) return query(tr<<1,l,r);
    else return query(tr<<1,l,mid)+query(tr<<1|1,mid+1,r);
}

 

 

 

 三.例题

  如洛谷P3372 P3373(说实话,我的线段树不是最快的,但还可以)(我太菜了,只做了这些)

 

其实我的思路就是

维护线段树的性质,该更新时则更新,通过访问父节点,得到性质,节省访问子节点的时间,降低时间复杂度。

 

如有错误,恳请您指正(我太菜了);如有不理解,可留言,我会尽量回复。。。(高中生吐槽一波。。)

by Jackpei 2019.2.5

线段树

标签:决定   数列   com   info   心得   个人   区间   父节点   相同   

原文地址:https://www.cnblogs.com/Jackpei/p/10352817.html

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