zoukankan      html  css  js  c++  java
  • 浅谈线段树

    博客内容主要来自https://www.cnblogs.com/TheRoadToTheGold/p/6254255.html

    感谢原博主大大,代码部分我根据我的习惯进行了更改

    数据结构——线段树

    1、引例

    A.给出n个数,n<=100,和m个询问,每次询问区间[l,r]的和,并输出。

    一种回答:这也太简单了,O(n)枚举搜索就行了。

    另一种回答:还用得着o(n)枚举,前缀和o(1)就搞定。

    那好,我再修改一下题目。

    B.给出n个数,n<=100,和m个操作,每个操作可能有两种:1、在某个位置加上一个数;2、询问区间[l,r]的和,并输出。

    回答:o(n)枚举。

    动态修改最起码不能用静态的前缀和做了。

    好,我再修改题目:

    C.给出n个数,n<=1000000,和m个操作,每个操作可能有两种:1、在某个位置加上一个数;2、询问区间[l,r]的和,并输出。

    回答:o(n)枚举绝对超时。

    再改:

    D,给出n个数,n<=1000000,和m个操作,每个操作修改一段连续区间[a,b]的值

    回答:从a枚举到b,一个一个改。。。。。。有点儿常识的人都知道超时

    那怎么办?这就需要一种强大的数据结构:线段树。

    二、基本概念

    1、线段树是一棵二叉搜索树,它储存的是一个区间的信息。

    2、每个节点以结构体的方式存储,结构体包含以下几个信息:

         区间左端点、右端点;(这两者必有)

         这个区间要维护的信息(事实际情况而定,数目不等)。

    3、线段树的基本思想:二分

    4、线段树一般结构如图所示:

    5、特殊性质:

    由上图可得,

    1、每个节点的左孩子区间范围为[l,mid],右孩子为[mid+1,r]

    2、对于结点k,左孩子结点为2*k,右孩子为2*k+1,这符合完全二叉树的性质

    三、线段树的基础操作

    注:以下基础操作均以引例中的求和为例,结构体以此为例:

    struct node
    {
        int l;
        int r;
        int mid()
        {
            return (l+r)/2.0;
        }
        ll sum;//每一个节点的sum
        ll add;//延迟标记数组
    } tree[MAXN<<2];

    线段树的基础操作主要有5个:

    建树、单点查询、单点修改、区间查询、区间修改。

    1、建树,即建立一棵线段树

       ① 主体思路:a、对于二分到的每一个结点,给它的左右端点确定范围。

                             b、如果是叶子节点,存储要维护的信息。

                             c、状态合并。(这里的状态是求和)

      ②代码

    void Build(int l,int r,int i)
    {
        tree[i].l=l;
        tree[i].r=r;
        tree[i].add=0;///区间修改时需要
        tree[i].sum=0;
        if(l==r)///叶子节点
        {
            scanf("%lld",&tree[i].sum);///存储需要维护的信息
            return ;///注意!!
        }
        int m=tree[i].mid();
        Build(l,m,i<<1);///左孩子
        Build(m+1,r,i<<1|1);///右孩子
        push_up(i);///向上回溯
    }

    根据题目要求写状态合并的函数,这个给出一个区间求和的函数

    void push_up(int i)
    {
        tree[i].sum=tree[i<<1].sum+tree[i<<1|1].sum;
    }

    ③注意

     a.结构体要开4倍空间,为啥自己画一个[1,10]的线段树就懂了

     b.千万不要漏了return语句,因为到了叶子节点不需要再继续递归了。

    2、单点查询,即查询一个点的状态,设待查询点为x

       ①主体思路:与二分查询法基本一致,如果当前枚举的点左右端点相等,即叶子节点,就是目标节点。如果不是,因为这是二分法,所以设查询位置为x,当前结点区间范围为了l,r,中点为 mid,则如果x<=mid,则递归它的左孩子,否则递归它的右孩子

       ②代码

    int query(int x,int i)///单点查询
    {
        if(tree[i].l==tree[i].r)///叶子节点,同时也是目目标节点
        {
            return tree[i].sum;
        }int m=tree[i].mid();
        if(x<=m)
        {
             return query(x,i<<1);///左孩子
        }
        else
        {
             return query(x,i<<1|1);///右孩子
        }
    }

      ③正确性分析:

         因为如果不是目标位置,由if—else语句对目标位置定位,逐步缩小目标范围,最后一定能只到达目标叶子节点。

    3、单点修改,即更改某一个点的状态。用引例中的例子,对第x个数加上y

    ①主体思路

     结合单点查询的原理,找到x的位置;根据建树状态合并的原理,修改每个结点的状态。

     ②代码

    void update(int l,int r,int i,int v,int num)
    {
        if(l==r&&l==num)///找到目标位置
        {
            tree[i].value=v;
            return ;
        }
        int m=tree[i].mid();
        if(m>=num)
        {
            update(l,m,i<<1,v,num);
        }
        else
        {
            update(m+1,r,i<<1|1,v,num);
        }
        push_up(i);
    }

    4、区间查询,即查询一段区间的状态,在引例中为查询区间[x,y]的和

    ①主体思路

     

     

    mid=(l+r)/2

    y<=mid ,即 查询区间全在,当前区间的左子区间,往左孩子走

    x>mid 即 查询区间全在,当前区间的右子区间,往右孩子走

    否则,两个子区间都走

    ②代码

    void query(int l,int r,int i)
    {
        if(tree[i].l==l&&tree[i].r==r)///叶子节点,同时也是目目标节点
        {
            ans+=tree[i].sum;
            return ;
        }
        ///push_down(i,tree[i].r-tree[i].l+1);区间修改时使用
        int m=tree[i].mid();
        if(r<=m)
        {
            query(l,r,i<<1);///目标位置比中点靠左,递归到左孩子
        }
        else if(l>m)///目标位置比中点靠右,递归到右孩子
        {
            query(l,r,i<<1|1);
        }
        else///占两段
        {
            query(l,m,i<<1);///左孩子
            query(m+1,r,i<<1|1);///右孩子
        }
    }

    ③正确性分析

    情况1,3不用说,对于情况2,最差情况是搜到叶子节点,此时一定满足情况1

    5、区间修改,即修改一段连续区间的值,我们已给区间[a,b]的每个数都加x为例讲解

       

    Ⅰ.引子

           有人可能就想到了:

           修改的时候只修改对查询有用的点。

           对,这就是区间修改的关键思路。

          为了实现这个,我们引入一个新的状态——懒标记

      Ⅱ 懒标记

         (懒标记比较难理解,我尽力讲明白。。。。。。)

           1、直观理解:“懒”标记,懒嘛!用到它才动,不用它就睡觉。

           2、作用:存储到这个节点的修改信息,暂时不把修改信息传到子节点。就像家长扣零花钱,你用的时候才给你,不用不给你。

           3、实现思路(重点):

               a.原结构体中增加新的变量,存储这个懒标记。

               b.递归到这个节点时,只更新这个节点的状态,并把当前的更改值累积到标记中。注意是累积,可以这样理解:过年,很多个亲戚都给你压岁钱,但你暂时不用,所以都被你父母扣下了。

               c.什么时候才用到这个懒标记?当需要递归这个节点的子节点时,标记下传给子节点。这里不必管用哪个子节点,两个都传下去。就像你如果还有妹妹,父母给你们零花钱时总不能偏心吧

               d.下传操作:

                   3部分:①当前节点的懒标记累积到子节点的懒标记中。

                                 ②修改子节点状态。在引例中,就是原状态+子节点区间点的个数*父节点传下来的懒标记

                                 这就有疑问了,既然父节点都把标记传下来了,为什么还要乘父节点的懒标记,乘自己的不行吗?

                                 因为自己的标记可能是父节点多次传下来的累积,每次都乘自己的懒标记造成重复累积

                                ③父节点懒标记清0。这个懒标记已经传下去了,不清0后面再用这个懒标记时会重复下传。就像你父母给了你5元钱,你不能说因为前几次给了你10元钱, 所以这次给了你15元,那你不就亏大了。 

         懒标记下穿代码:

    void push_down(int i,int L)///L为区间长度
    {
        if(tree[i].add)
        {
            tree[i<<1].add+=tree[i].add;
            tree[i<<1|1].add+=tree[i].add;
            tree[i<<1].sum+=tree[i].add*(L-(L>>1));
            tree[i<<1|1].sum+=tree[i].add*(L>>1);
            tree[i].add=0;
        }
    }

     

     Ⅲ 完整的区间修改代码:

    void update(int l,int r,int i,int v)
    {
        if(tree[i].l==l&&tree[i].r==r)///找到目标位置
        {
            tree[i].sum+=(ll)v*(r-l+1);
            tree[i].add+=(ll)v;///懒标记+v
            return ;
        }
        push_down(i,tree[i].r-tree[i].l+1);///懒标记下传
        int m = tree[i].mid();
        if(r<=m)
        {
            update(l,r,i<<1,v);
        }
        else if(l>m)
        {
            update(l,r,i<<1|1,v);
        }
        else
        {
            update(l,m,i<<1,v);///左孩子
            update(m+1,r,i<<1|1,v);///右孩子
        }
        push_up(i);///向上回溯,更改区间状态
    }

     Ⅳ.懒标记的引入对其他基本操作的影响

         因为引入了懒标记,很多用不着的更改状态存了起来,这就会对区间查询、单点查询造成一定的影响。

         所以在使用了懒标记的程序中,单点查询、区间查询也要像区间修改那样,对用得到的懒标记下传。其实就是加上一句   push_down(i,tree[i].r-tree[i].l+1);  

    三、总结

    模板:

      1 #include<cstdio>
      2 #include<cstring>
      3 #include<algorithm>
      4 #define ll long long int
      5 ll ans;
      6 const int MAXN=2e5+10;
      7 using namespace std;
      8 struct node
      9 {
     10     int l;
     11     int r;
     12     int mid()
     13     {
     14         return (l+r)/2.0;
     15     }
     16     ll sum;///每一个节点的sum
     17     ll add;///延迟标记数组
     18 } tree[MAXN<<2];
     19 void push_up(int i)
     20 {
     21     tree[i].sum=tree[i<<1].sum+tree[i<<1|1].sum;
     22 }
     23 void push_down(int i,int L)///L为区间长度
     24 {
     25     if(tree[i].add)
     26     {
     27         tree[i<<1].add+=tree[i].add;
     28         tree[i<<1|1].add+=tree[i].add;
     29         tree[i<<1].sum+=tree[i].add*(L-(L>>1));
     30         tree[i<<1|1].sum+=tree[i].add*(L>>1);
     31         tree[i].add=0;
     32     }
     33 }
     34 void Build(int l,int r,int i)
     35 {
     36     tree[i].l=l;
     37     tree[i].r=r;
     38     tree[i].add=0;
     39     tree[i].sum=0;
     40     if(l==r)///叶子节点
     41     {
     42         scanf("%lld",&tree[i].sum);///存储需要维护的信息
     43         return ;
     44     }
     45     int m=tree[i].mid();
     46     Build(l,m,i<<1);///左孩子
     47     Build(m+1,r,i<<1|1);///右孩子
     48     push_up(i);///向上回溯
     49 }
     50 void query(int l,int r,int i)
     51 {
     52     if(tree[i].l==l&&tree[i].r==r)///叶子节点,同时也是目目标节点
     53     {
     54         ans+=tree[i].sum;
     55         return ;
     56     }
     57     push_down(i,tree[i].r-tree[i].l+1);
     58     int m=tree[i].mid();
     59     if(r<=m)
     60     {
     61         query(l,r,i<<1);///目标位置比中点靠左,递归到左孩子
     62     }
     63     else if(l>m)///目标位置比中点靠右,递归到右孩子
     64     {
     65         query(l,r,i<<1|1);
     66     }
     67     else///占两段
     68     {
     69         query(l,m,i<<1);///左孩子
     70         query(m+1,r,i<<1|1);///右孩子
     71     }
     72     /*if(l<=m)
     73     {
     74          query(l,m,i<<1);///左孩子
     75     }
     76     if(m<r)
     77     {
     78          query(m+1,r,i<<1|1);///右孩子
     79     }*/
     80 }
     81 
     82 void update(int l,int r,int i,int v)
     83 {
     84     if(tree[i].l==l&&tree[i].r==r)///找到目标位置
     85     {
     86         tree[i].sum+=(ll)v*(r-l+1);
     87         tree[i].add+=(ll)v;///懒标记+v
     88         return ;
     89     }
     90     push_down(i,tree[i].r-tree[i].l+1);///懒标记下传
     91     int m = tree[i].mid();
     92     if(r<=m)
     93     {
     94         update(l,r,i<<1,v);
     95     }
     96     else if(l>m)
     97     {
     98         update(l,r,i<<1|1,v);
     99     }
    100     else
    101     {
    102         update(l,m,i<<1,v);///左孩子
    103         update(m+1,r,i<<1|1,v);///右孩子
    104     }
    105     /*
    106     if(l<=m)
    107     {
    108         update(l,m,i<<1,v);///左孩子
    109     }
    110     if(m<r)
    111     {
    112         update(m+1,r,i<<1|1,v);///右孩子
    113     }*/
    114     push_up(i);///向上回溯,更改区间状态
    115 }
    116 int main()
    117 {
    118     int n,m,a,b,d;
    119     char c;
    120     scanf("%d%d",&n,&m);
    121     Build(1,n,1);///前两个参数是节点的左右端点,最后一个参数是节点在结构体中的位置
    122     while(m--)
    123     {
    124         scanf(" %c",&c);
    125         if(c=='Q')
    126         {
    127             ans=0;
    128             scanf("%d%d",&a,&b);
    129             query(a,b,1);///同Build
    130             printf("%lld
    ",ans);
    131         }
    132         else if(c=='C')
    133         {
    134             scanf("%d%d%d",&a,&b,&d);
    135             update(a,b,1,d);
    136         }
    137     }
    138     return 0;
    139 }
    View Code
  • 相关阅读:
    World file文件格式
    HTML5 基础
    Spring Framework---概况
    Tomcat(1)
    警言妙句
    嵌入式系统总是要用户对变量或寄存器进行位操作。给定一个整型变量a,写两段代码,第一个设置a的bit 3,第二个清除a 的bit 3。在以上两个操作中,要保持其它位不变。
    关键字volatile有什么含意?并给出三个不同的例子。
    关键字const有什么含意?
    关于指针数组、数组指针、指针函数、函数指针等的问题
    实现两个int变量的值的交换,要求不使用临时变量。
  • 原文地址:https://www.cnblogs.com/wkfvawl/p/9397657.html
Copyright © 2011-2022 走看看