• 浅谈线段树


    作为数据结构专场的开端,也是最基础的数据结构之一,愿大家有个好的开始!!!

    先来一道毒瘤题,仅供参考,希望初学者不要在意 https://www.luogu.org/problemnew/show/P3373

    O、引例

    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,r,w;//l,r分别表示区间左右端点,w表示区间和
    }tree[4*n+1];

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

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

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

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

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

                         c、状态合并。

      ②代码

     1 #include<cstdio>
     2 using namespace std;
     3 struct sd{
     4      int l,r,w;
     5 }
     6 sd tree[10005];
     7 int ans;
     8 void build(int l,int r,int k)
     9 {
    10     tree[k].l=l;
    11     tree[k].r=r;
    12     if(l==r)
    13     {
    14         scanf("%d",&tree[k].w);
    15         return ;
    16     }
    17     int m=(l+r)/2;
    18     build(l,m,k*2);
    19     build(m+1,r,k*2+1);
    20     tree[k].w=tree[k*2].w+tree[k*2+1].w;
    21 }
    22 int main()
    23 {
    24     int n;
    25     scanf("%d",&n);
    26     build(1,n,1);
    27     return 0;
    28 }

    ③注意

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

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

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

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

     1 #include<cstdio>
     2 using namespace std;
     3 struct sd{
     4      int l,r,w;
     5 }
     6 sd tree[10005];
     7 int ans;
     8 int x;//目标查询点。
     9 void search(v)
    10 {
    11      if(tree[v].l==tree[v].r)
    12      {
    13          ans=tree[v].w;
    14          return;
    15      }
    16      int m=(tree[v].l+tree[v].r)/2;
    17      if(x<=m)find(v*2);
    18      if(x>m)find(v*2+1);
    19 }

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

    ①主体思路

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

     

     1 #include<cstdio>
     2 using namespace std;
     3 struct sd{
     4      int l,r,w;
     5 }
     6 sd tree[10005];
     7 int change;
     8 int x;//要修改的点。
     9 void search(v)
    10 {
    11      if(tree[v].l==tree[v].r)
    12      {
    13          tree[v].w=tree[v].w+change;
    14          return;
    15      }
    16      int m=(tree[v].l+tree[v].r)/2;
    17      if(x<=m)find(v*2);
    18      if(x>m)find(v*2+1);
    19      tree[v].w=tree[v*2].w+tree[v*2+1].w;
    20 }

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

    正确性分析

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

    代码

    #include<cstdio>
    using namespace std;
    struct sd{
         int l,r,w;
    }
    sd tree[10005];
    int x,y;//查询的目标区间。
    int res=0;
    void make(int k)
    {
        if(tree[k].l>=x&&tree[k].r<=y)
        {
            res=res+tree[k].w;//根据题目要求。
            return ;
        }
        int m=(tree[k].l+tree[k].r)/2;
        if(m<y)make(k*2+1);
        if(m>=x)make(k*2);
    }

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

        Ⅰ.引子

    代码

     1 #include<cstdio>
     2 using namespace std;
     3 struct sd{
     4      int l,r,w;
     5 }
     6 sd tree[10005];
     7 int x,y;//查询的目标区间。
     8 int change;
     9 void make(int k)
    10 {
    11     if(tree[k].l>=x&&tree[k].r<=y&&tree[k].l==tree[k].r)
    12     {
    13         tree[k].w=tree[k].w+change;
    14         return ;
    15     }
    16     int m=(tree[k].l+tree[k].r)/2;
    17     if(m<y)make(k*2+1);
    18     if(m>=x)make(k*2);
    19     tree[k].w=tree[k*2].w+tree[k*2+1].w;
    20 }

    有人可能就想到了:

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

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

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

      Ⅱ 懒标记

         (懒标记比较难理解,自行领悟)

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

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

           3、实现思路(重点):

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

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

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

               d.下传操作:

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

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

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

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

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

         懒标记下传代码:f为懒标记,其余变量与前面含义一致。

     1 #include<cstdio>
     2 using namespace std;
     3 struct sd{
     4      int l,r,w;
     5      int f;//懒标记。
     6 }
     7 sd tree[10005];
     8 int x,y;//查询的目标区间。
     9 void down(int k)//下传操作
    10 {
    11     tree[k*2].f+=tree[k].f;
    12     tree[k*2+1].f+=tree[k].f;
    13     tree[k*2].w+=tree[k].f*(tree[k*2].r-tree[k*2].l+1);
    14     tree[k*2+1].w+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1);
    15     tree[k].f=0;
    16 }
    17 void add(int k)//修改操作
    18 {
    19     if(tree[k].l>=a&&tree[k].r<=b)//当前区间全部对要修改的区间有用 
    20     {
    21         tree[k].w+=(tree[k].r-tree[k].l+1)*x;//区间点的总数
    22         tree[k].f+=x;
    23         return;
    24     }
    25     if(tree[k].f) down(k);
    26     //懒标记下传。只有不满足上面if条件才执行,所以一定会用到当前节点的子节点
    27     int m=(tree[k].l+tree[k].r)/2;
    28     if(a<=m) add(k*2);
    29     if(b>m) add(k*2+1);
    30     tree[k].w=tree[k*2].w+tree[k*2+1].w;//更改区间状态 
    31 }

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

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

         所以在使用了懒标记的程序中,单点查询、区间查询也要像区间修改那样,对用得到的懒标记下传。其实就是加上一句if(tree[k].f)  down(k),其余不变。

    引入了懒标记的单点查询代码:

     1 void ask(int k)//单点查询
     2 {
     3     if(tree[k].l==tree[k].r)
     4     {
     5         ans=tree[k].w;
     6         return ;
     7     }
     8     if(tree[k].f) down(k);//懒标记下传,唯一需要更改的地方
     9     int m=(tree[k].l+tree[k].r)/2;
    10     if(x<=m) ask(k*2);
    11     else ask(k*2+1);
    12 }

    引入了懒标记的区间查询代码:

     1 void sum(int k)//区间查询
     2 {
     3     if(tree[k].l>=x&&tree[k].r<=y) 
     4     {
     5         ans+=tree[k].w;
     6         return;
     7     }
     8     if(tree[k].f)  down(k)//懒标记下传,唯一需要更改的地方
     9     int m=(tree[k].l+tree[k].r)/2;
    10     if(x<=m) sum(k*2);
    11     if(y>m) sum(k*2+1);
    12 }

    三、总结

    线段树5种基本操作代码:

      1 #include<cstdio>
      2 using namespace std;
      3 int n,p,a,b,m,x,y,ans;
      4 struct node
      5 {
      6     int l,r,w,f;
      7 }tree[400001];
      8 inline void build(int k,int ll,int rr)//建树 
      9 {
     10     tree[k].l=ll,tree[k].r=rr;
     11     if(tree[k].l==tree[k].r)
     12     {
     13         scanf("%d",&tree[k].w);
     14         return;
     15     }
     16     int m=(ll+rr)/2;
     17     build(k*2,ll,m);
     18     build(k*2+1,m+1,rr);
     19     tree[k].w=tree[k*2].w+tree[k*2+1].w;
     20 }
     21 inline void down(int k)//标记下传 
     22 {
     23     tree[k*2].f+=tree[k].f;
     24     tree[k*2+1].f+=tree[k].f;
     25     tree[k*2].w+=tree[k].f*(tree[k*2].r-tree[k*2].l+1);
     26     tree[k*2+1].w+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1);
     27     tree[k].f=0;
     28 }
     29 inline void ask_point(int k)//单点查询
     30 {
     31     if(tree[k].l==tree[k].r)
     32     {
     33         ans=tree[k].w;
     34         return ;
     35     }
     36     if(tree[k].f) down(k);
     37     int m=(tree[k].l+tree[k].r)/2;
     38     if(x<=m) ask_point(k*2);
     39     else ask_point(k*2+1);
     40 }
     41 inline void change_point(int k)//单点修改 
     42 {
     43     if(tree[k].l==tree[k].r)
     44     {
     45         tree[k].w+=y;
     46         return;
     47     }
     48     if(tree[k].f) down(k);
     49     int m=(tree[k].l+tree[k].r)/2;
     50     if(x<=m) change_point(k*2);
     51     else change_point(k*2+1);
     52     tree[k].w=tree[k*2].w+tree[k*2+1].w; 
     53 }
     54 inline void ask_interval(int k)//区间查询 
     55 {
     56     if(tree[k].l>=a&&tree[k].r<=b) 
     57     {
     58         ans+=tree[k].w;
     59         return;
     60     }
     61     if(tree[k].f) down(k);
     62     int m=(tree[k].l+tree[k].r)/2;
     63     if(a<=m) ask_interval(k*2);
     64     if(b>m) ask_interval(k*2+1);
     65 }
     66 inline void change_interval(int k)//区间修改 
     67 {
     68     if(tree[k].l>=a&&tree[k].r<=b)
     69     {
     70         tree[k].w+=(tree[k].r-tree[k].l+1)*y;
     71         tree[k].f+=y;
     72         return;
     73     }
     74     if(tree[k].f) down(k);
     75     int m=(tree[k].l+tree[k].r)/2;
     76     if(a<=m) change_interval(k*2);
     77     if(b>m) change_interval(k*2+1);
     78     tree[k].w=tree[k*2].w+tree[k*2+1].w;
     79 }
     80 int main()
     81 {
     82     scanf("%d",&n);//n个节点 
     83     build(1,1,n);//建树 
     84     scanf("%d",&m);//m种操作 
     85     for(int i=1;i<=m;i++)
     86     {
     87         scanf("%d",&p);
     88         ans=0;
     89         if(p==1)
     90         {
     91             scanf("%d",&x);
     92             ask_point(1);//单点查询,输出第x个数 
     93             printf("%d",ans);
     94         } 
     95         else if(p==2)
     96         {
     97             scanf("%d%d",&x,&y);
     98             change_point(1);//单点修改 
     99         }
    100         else if(p==3)
    101         {
    102             scanf("%d%d",&a,&b);//区间查询 
    103             ask_interval(1);
    104             printf("%d
    ",ans);
    105         }
    106         else
    107         {
    108              scanf("%d%d%d",&a,&b,&y);//区间修改 
    109              change_interval(1);
    110         }
    111     }
    112 }

     来一波纯线段树模板。

    #include<cstdio>
    #include<cstring>
    #include<cmath>
    #include<algorithm>
    using namespace std;
    struct sd{
        int l,r;
        long long w,f;
    };
    sd tree[400005];
    long long res;
    void down(int k)
    {
        tree[k*2].f+=tree[k].f;
        tree[k*2+1].f+=tree[k].f;
        tree[k*2].w+=tree[k].f*(tree[k*2].r-tree[k*2].l+1);
        tree[k*2+1].w+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1);
        tree[k].f=0;
    }
    void build(int l,int r,int k)
    {
        tree[k].l=l;
        tree[k].r=r;
        if(l==r)
        {
            scanf("%d",&tree[k].w);
            return;
        }
        int m=(l+r)/2;
        build(l,m,k*2);
        build(m+1,r,k*2+1);
        tree[k].w=tree[k*2].w+tree[k*2+1].w;
    }
    void add(int v,int x,int y,long long k)
    {
        if(tree[v].l>=x&&tree[v].r<=y)
        {
            tree[v].w+=(tree[v].r-tree[v].l+1)*k;
            tree[v].f+=k;
            return;
        }
        if(tree[v].f) down(v);
        int m=(tree[v].l+tree[v].r)/2;
        if(m<y)add(v*2+1,x,y,k);
        if(m>=x)add(v*2,x,y,k);
        tree[v].w=tree[v*2].w+tree[v*2+1].w;
    }
    
    void put(int v,int x,int y)
    {
        if(tree[v].l>=x&&tree[v].r<=y)
        {
            res=res+tree[v].w;
            return;
        }
        if(tree[v].f)down(v);
        int m=(tree[v].l+tree[v].r)/2;
        if(m<y)put(v*2+1,x,y);
        if(m>=x)put(v*2,x,y);
    }
    int main()
    {
        int n,m;
        scanf("%d%d",&n,&m);
        build(1,n,1);
        for(int i=1;i<=m;i++)
        {
            int order;
            scanf("%d",&order);
            if(order==1)
            {
                int x,y;
                long long k;
                scanf("%d%d%lld",&x,&y,&k);
                add(1,x,y,k);
            }
            if(order==2)
            {
                res=0;
                int x,y;
                scanf("%d%d",&x,&y);
                put(1,x,y);
                printf("%lld
    ",res);
            }
        }
        return 0;
    }

    最后来几道练习题

    http://codevs.cn/problem/1080/

    http://codevs.cn/problem/1081/

  • 相关阅读:
    报数游戏
    Dynamics CRM 2013 初体验(2):UI
    Lucene.net常见功能实现知识汇总
    hdu 1853 最小费用流好题 环的问题
    MessagerService总结
    [置顶] Android EditText/TextView使用SpannableString显示复合文本
    MySQL Dll语句
    JavaScript表单验证
    intellij—idea14 注冊机
    OL记载Arcgis Server切片
  • 原文地址:https://www.cnblogs.com/genius777/p/8470088.html
Copyright © 2020-2023  润新知