zoukankan      html  css  js  c++  java
  • 线段树笔记

    数据结构——线段树

    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,左孩子结点为2k,右孩子为2k+1,这符合完全二叉树的性质

    二、线段树的基础操作

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

    struct node
    {
           int l,r,w;//l,r分别表示区间左右端点,w表示区间和
    }tree[4*n+1];
    

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

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

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

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

                     b、如果是叶子节点,存储要维护的信息。
    
                     c、状态合并。
    

    ②代码

    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;//状态合并,此结点的w=两个孩子的w之和 
    }
    

    ③注意

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

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

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

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

    ②代码

    void ask(int k)
    {
        if(tree[k].l==tree[k].r) //当前结点的左右端点相等,是叶子节点,是最终答案 
        {
            ans=tree[k].w;
            return ;
        }
        int m=(tree[k].l+tree[k].r)/2;
        if(x<=m) ask(k*2);//目标位置比中点靠左,就递归左孩子 
        else ask(k*2+1);//反之,递归右孩子 
    }
    

    ③正确性分析:

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

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

    ①主体思路

    结合单点查询的原理,找到x的位置;根据建树状态合并的原理,修改每个结点的状态。
    在这里插入图片描述②代码

    void add(int k)
    {
        if(tree[k].l==tree[k].r)//找到目标位置 
        {
            tree[k].w+=y;
            return;
        }
        int m=(tree[k].l+tree[k].r)/2;
        if(x<=m) add(k*2);
        else add(k*2+1);
        tree[k].w=tree[k*2].w+tree[k*2+1].w;//所有包含结点k的结点状态更新 
    }
    

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

    ①主体思路
    在这里插入图片描述(mid=(l+r)/2)

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

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

    否则,两个子区间都走

    void sum(int k)
    {
        if(tree[k].l>=x&&tree[k].r<=y) 
        {
            ans+=tree[k].w;
            return;
        }
        int m=(tree[k].l+tree[k].r)/2;
        if(x<=m) sum(k*2);
        if(y>m) sum(k*2+1);
    }
    

    ③正确性分析

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

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

    在这里插入图片描述
    有人可能就想到了:

       修改的时候只修改对查询有用的点。
    
       对,这就是区间修改的关键思路。
    
      为了实现这个,我们引入一个新的状态——懒标记。
    

    Ⅱ 懒标记

     (懒标记比较难理解,我尽力讲明白。。。。。。)
    
       1、直观理解:“懒”标记,懒嘛!用到它才动,不用它就睡觉。
    
       2、作用:存储到这个节点的修改信息,暂时不把修改信息传到子节点。就像家长扣零花钱,你用的时候才给你,不用不给你。
    
       3、实现思路(重点):
    
           a.原结构体中增加新的变量,存储这个懒标记。
    
           b.递归到这个节点时,只更新这个节点的状态,并把当前的更改值累积到标记中。注意是累积,可以这样理解:过年,很多个亲戚都给你压岁钱,但你暂时不用,所以都被你父母扣下了。
    
           c.什么时候才用到这个懒标记?当需要递归这个节点的子节点时,标记下传给子节点。这里不必管用哪个子节点,两个都传下去。就像你如果还有妹妹,父母给你们零花钱时总不能偏心吧
    
           d.下传操作:
    
               3部分:①当前节点的懒标记累积到子节点的懒标记中。
    
                         ②修改子节点状态。在引例中,就是原状态+子节点区间点的个数*父节点传下来的懒标记。
    
                            这就有疑问了,既然父节点都把标记传下来了,为什么还要乘父节点的懒标记,乘自己的不行吗?
    
                            因为自己的标记可能是父节点多次传下来的累积,每次都乘自己的懒标记造成重复累积
    
                         ③父节点懒标记清0。这个懒标记已经传下去了,不清0后面再用这个懒标记时会重复下传。就像你父母给了你5元钱,你不能说因为前几次给了你10元钱, 所以这次给了你15元,那你不就亏大了。 
    
     懒标记下穿代码:f为懒标记,其余变量与前面含义一致。
    
    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 add(int k)
    {
        if(tree[k].l>=a&&tree[k].r<=b)//当前区间全部对要修改的区间有用 
        {
            tree[k].w+=(tree[k].r-tree[k].l+1)*x;//(r-1)+1区间点的总数
            tree[k].f+=x;
            return;
        }
        if(tree[k].f) down(k);//懒标记下传。只有不满足上面的if条件才执行,所以一定会用到当前节点的子节点 
        int m=(tree[k].l+tree[k].r)/2;
        if(a<=m) add(k*2);
        if(b>m) add(k*2+1);
        tree[k].w=tree[k*2].w+tree[k*2+1].w;//更改区间状态 
    }
    

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

     因为引入了懒标记,很多用不着的更改状态存了起来,这就会对区间查询、单点查询造成一定的影响。
    
     所以在使用了懒标记的程序中,单点查询、区间查询也要像区间修改那样,对用得到的懒标记下传。其实就是加上一句if(tree[k].f)  down(k),其余不变。
    
     2017.5.16 之前写的单点修改不需要下传懒标记,在此订正:单点修改也需要下传懒标记
    
     引入了懒标记的单点查询代码:
    
    void ask(int k)//单点查询
    {
        if(tree[k].l==tree[k].r)
        {
            ans=tree[k].w;
            return ;
        }
        if(tree[k].f) down(k);//懒标记下传,唯一需要更改的地方
        int m=(tree[k].l+tree[k].r)/2;
        if(x<=m) ask(k*2);
        else ask(k*2+1);
    }
    
    引入了懒标记的区间查询代码:
    
    void sum(int k)//区间查询
    {
        if(tree[k].l>=x&&tree[k].r<=y) 
        {
            ans+=tree[k].w;
            return;
        }
        if(tree[k].f)  down(k)//懒标记下传,唯一需要更改的地方
        int m=(tree[k].l+tree[k].r)/2;
        if(x<=m) sum(k*2);
        if(y>m) sum(k*2+1);
    }
    

    三、总结

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

    #include<cstdio>
    using namespace std;
    int n,p,a,b,m,x,y,ans;
    struct node
    {
        int l,r,w,f;
    }tree[400001];
    inline void build(int k,int ll,int rr)//建树 
    {
        tree[k].l=ll,tree[k].r=rr;
        if(tree[k].l==tree[k].r)
        {
            scanf("%d",&tree[k].w);
            return;
        }
        int m=(ll+rr)/2;
        build(k*2,ll,m);
        build(k*2+1,m+1,rr);
        tree[k].w=tree[k*2].w+tree[k*2+1].w;
    }
    inline 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;
    }
    inline void ask_point(int k)//单点查询
    {
        if(tree[k].l==tree[k].r)
        {
            ans=tree[k].w;
            return ;
        }
        if(tree[k].f) down(k);
        int m=(tree[k].l+tree[k].r)/2;
        if(x<=m) ask_point(k*2);
        else ask_point(k*2+1);
    }
    inline void change_point(int k)//单点修改 
    {
        if(tree[k].l==tree[k].r)
        {
            tree[k].w+=y;
            return;
        }
        if(tree[k].f) down(k);
        int m=(tree[k].l+tree[k].r)/2;
        if(x<=m) change_point(k*2);
        else change_point(k*2+1);
        tree[k].w=tree[k*2].w+tree[k*2+1].w; 
    }
    inline void ask_interval(int k)//区间查询 
    {
        if(tree[k].l>=a&&tree[k].r<=b) 
        {
            ans+=tree[k].w;
            return;
        }
        if(tree[k].f) down(k);
        int m=(tree[k].l+tree[k].r)/2;
        if(a<=m) ask_interval(k*2);
        if(b>m) ask_interval(k*2+1);
    }
    inline void change_interval(int k)//区间修改 
    {
        if(tree[k].l>=a&&tree[k].r<=b)
        {
            tree[k].w+=(tree[k].r-tree[k].l+1)*y;
            tree[k].f+=y;
            return;
        }
        if(tree[k].f) down(k);
        int m=(tree[k].l+tree[k].r)/2;
        if(a<=m) change_interval(k*2);
        if(b>m) change_interval(k*2+1);
        tree[k].w=tree[k*2].w+tree[k*2+1].w;
    }
    int main()
    {
        scanf("%d",&n);//n个节点 
        build(1,1,n);//建树 
        scanf("%d",&m);//m种操作 
        for(int i=1;i<=m;i++)
        {
            scanf("%d",&p);
            ans=0;
            if(p==1)
            {
                scanf("%d",&x);
                ask_point(1);//单点查询,输出第x个数 
                printf("%d",ans);
            } 
            else if(p==2)
            {
                scanf("%d%d",&x,&y);
                change_point(1);//单点修改 
            }
            else if(p==3)
            {
                scanf("%d%d",&a,&b);//区间查询 
                ask_interval(1);
                printf("%d
    ",ans);
            }
            else
            {
                 scanf("%d%d%d",&a,&b,&y);//区间修改 
                 change_interval(1);
            }
        }
    }
    

    四、空间优化

    父节点k,左二子2k,右儿子2k+1,需要4*n的空间

    但并不是所有的叶子节点占用到2n+1——4n

    这就造成大量空间浪费

    2*n空间表示法:推荐博客:http://www.cppblog.com/MatoNo1/archive/2015/05/05/195857.html

    用dfs序表示做节点下标

    父节点k,左儿子k+1,右儿子:k+左儿子区间长度*2,不是父节点下标+父节点区间长度。因为当树不满时,两者不相等

    具体实现这里就不再写模板了,就是改改左右儿子的下标

    可参考代码: 题目:楼房重建 http://www.cnblogs.com/TheRoadToTheGold/p/6361242.html

    里面的建树用的2*n空间

    五、模板题

    1、codevs 1080 线段树练习 (单点修改+区间查询) http://codevs.cn/problem/1080/

    #include<cstdio>
    using namespace std;
    int n,m,p,x,y,ans;
    struct node
    {
        int l,r,w;
    }tree[400001];
    inline 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;
    }
    inline void add(int k)
    {
        if(tree[k].l==tree[k].r)
        {
            tree[k].w+=y;
            return;
        }
        int m=(tree[k].l+tree[k].r)/2;
        if(x<=m) add(k*2);
        else add(k*2+1);
        tree[k].w=tree[k*2].w+tree[k*2+1].w; 
    }
    inline void sum(int k)
    {
        if(tree[k].l>=x&&tree[k].r<=y) 
        {
            ans+=tree[k].w;
            return;
        }
        int m=(tree[k].l+tree[k].r)/2;
        if(x<=m) sum(k*2);
        if(y>m) sum(k*2+1);
    }
    int main()
    {
        scanf("%d",&n);
        build(1,n,1);
        scanf("%d",&m);
        for(int i=1;i<=m;i++)
        {
            scanf("%d%d%d",&p,&x,&y);
            ans=0;
            if(p==1) add(1);
            else 
            {
                sum(1);
                printf("%d
    ",ans);
            }
        }
    }
    

    2、codevs 1081 线段树练习2 (单点查询+区间修改) http://codevs.cn/problem/1081/

    #include<cstdio>
    using namespace std;
    int n,p,a,b,m,x,ans;
    struct node
    {
        int l,r,w,f;
    }tree[400001];
    inline void build(int k,int ll,int rr)
    {
        tree[k].l=ll,tree[k].r=rr;
        if(tree[k].l==tree[k].r)
        {
            scanf("%d",&tree[k].w);
            return;
        }
        int m=(ll+rr)/2;
        build(k*2,ll,m);
        build(k*2+1,m+1,rr);
        tree[k].w=tree[k*2].w+tree[k*2+1].w;
    }
    inline 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;
    }
    inline void add(int k)
    {
        if(tree[k].l>=a&&tree[k].r<=b)
        {
            tree[k].w+=(tree[k].r-tree[k].l+1)*x;
            tree[k].f+=x;
            return;
        }
        if(tree[k].f) down(k);
        int m=(tree[k].l+tree[k].r)/2;
        if(a<=m) add(k*2);
        if(b>m) add(k*2+1);
        tree[k].w=tree[k*2].w+tree[k*2+1].w;
    }
    inline void ask(int k)
    {
        if(tree[k].l==tree[k].r)
        {
            ans=tree[k].w;
            return;
        }
        if(tree[k].f) down(k);
        int m=(tree[k].l+tree[k].r)/2;
        if(x<=m) ask(k*2);
        else ask(k*2+1); 
    }
    int main()
    {
        scanf("%d",&n);
        build(1,1,n);
        scanf("%d",&m);
        for(int i=1;i<=m;i++)
        {
            scanf("%d",&p);
            if(p==1)
            {
                scanf("%d%d%d",&a,&b,&x);
                add(1);
            }
            else
            {
                scanf("%d",&x);
                ask(1);
                printf("%d
    ",ans);
            }
        }
    }
    

    3、codevs 1082 线段树练习3 (区间修改+区间查询)

    #include<cstdio>
    using namespace std;
    int n,p,a,b,m,x,y;
    long long ans;
    struct node
    {
        long long l,r,w,f;
    }tree[800001];
    inline void build(int k,int ll,int rr)//建树 
    {
        tree[k].l=ll,tree[k].r=rr;
        if(tree[k].l==tree[k].r)
        {
            scanf("%d",&tree[k].w);
            return;
        }
        int m=(ll+rr)/2;
        build(k*2,ll,m);
        build(k*2+1,m+1,rr);
        tree[k].w=tree[k*2].w+tree[k*2+1].w;
    }
    inline 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;
    }
    inline void ask_interval(int k)//区间查询 
    {
        if(tree[k].l>=a&&tree[k].r<=b) 
        {
            ans+=tree[k].w;
            return;
        }
        if(tree[k].f) down(k);
        int m=(tree[k].l+tree[k].r)/2;
        if(a<=m) ask_interval(k*2);
        if(b>m) ask_interval(k*2+1);
    }
    inline void change_interval(int k)//区间修改 
    {
        if(tree[k].l>=a&&tree[k].r<=b)
        {
            tree[k].w+=(tree[k].r-tree[k].l+1)*y;
            tree[k].f+=y;
            return;
        }
        if(tree[k].f) down(k);
        int m=(tree[k].l+tree[k].r)/2;
        if(a<=m) change_interval(k*2);
        if(b>m) change_interval(k*2+1);
        tree[k].w=tree[k*2].w+tree[k*2+1].w;
    }
    int main()
    {
        scanf("%d",&n); 
        build(1,1,n);
        scanf("%d",&m);
        for(int i=1;i<=m;i++)
        {
            scanf("%d",&p);
            ans=0;
            if(p==1) 
            {
                 scanf("%d%d%d",&a,&b,&y);//区间修改 
                 change_interval(1);
            }
            else 
            {
                scanf("%d%d",&a,&b);//区间查询 
                ask_interval(1);
                printf("%lld
    ",ans);
            }
        
        }
    }
    

    六、经典例题

    codevs 3981/SPOJ GSS1/GSS3 ——区间最大子段和
    Bzoj3813 奇数国——区间内某个值是否出现过
    洛谷 P2894 酒店 Hotel ——区间连续一段空的长度
    codevs 2421 /Bzoj1858 序列操作——多种操作
    codevs 2000 / BZOJ 2957: 楼房重建——区间的最长上升子序列
    Codevs3044 矩形面积求并——扫描线

    代码的话到随笔分类——线段树里找找吧 http://www.cnblogs.com/TheRoadToTheGold/category/933602.html

    作者:xxy
    出处:http://www.cnblogs.com/TheRoadToTheGold/
    本文版权归作者和博客园共有,欢迎转载,但转载时请保留此段声明。

  • 相关阅读:
    UITextView in iOS7 doesn&#39;t scroll
    interlliJ idea 不识别文件类型的解决方式
    __super
    自用广告过滤规则,整合xwhyc大大的,非常小才79K
    Eclipse设置打印线
    SQL Server批量替换全部表中内容sql语句-清楚挂马
    删除同样元素(线性表)
    学习node js 之微信公众帐号接口开发 准备工作之三
    Bitmap基本概念及在Android4.4系统上使用BitmapFactory的注意事项
    多个rs485设备怎样跟上位机通讯?
  • 原文地址:https://www.cnblogs.com/ShineEternal/p/10853062.html
Copyright © 2011-2022 走看看