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,左孩子结点为2*k,右孩子为2*k+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,左二子2*k,右儿子2*k+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/  

     View Code

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

     View Code

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

     View Code

    六、经典例题

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

    作者:xxy
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

    除特别注明外,本站所有文章均为Manjusaka丶梦寒原创,转载请注明来自出处

  • 相关阅读:
    JavaScript cookie详解
    Javascript数组的排序:sort()方法和reverse()方法
    javascript中write( ) 和 writeln( )的区别
    div做表格
    JS 盒模型 scrollLeft, scrollWidth, clientWidth, offsetWidth 详解
    Job for phpfpm.service failed because the control process exited with error code. See "systemctl status phpfpm.service" and "journalctl xe" for details.
    orm查询存在价格为空问题
    利用救援模式破解系统密码
    SSH服务拒绝了密码
    C# 调用 C++ DLL 中的委托,引发“对XXX::Invoke类型的已垃圾回收委托进行了回调”错误的解决办法
  • 原文地址:https://www.cnblogs.com/rmy020718/p/8832889.html
Copyright © 2011-2022 走看看