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

    概述:

    线段树,类似区间树,它在各个节点保存一条线段(数组中的一段子数组)

    主要用于高效解决连续区间的动态查询问题,由于二叉结构的特性,它基本能保持每个操作的复杂度为O(logn)。

    线段树的每个节点表示一个区间,子节点则分别表示父节点的左右半区间

    例如父亲的区间是[a,b],那么(c=(a+b)/2)左儿子的区间是[a,c],右儿子的区间是[c+1,b]。

     问题描述如下:

    从数组arr[0...n-1]中查找某个数组某个区间内的最小值,其中数组大小固定,但是数组中的元素的值可以随时更新。

    (1)遍历数组区间找到最小值,时间复杂度是O(n),额外的空间复杂度O(1)。

    当数据量特别大,而查询操作很频繁的时候,耗时可能会不满足需求。

    这是一种解法

    (2)使用一个二维数组来保存提前计算好的区间[i,j]内的最小值,那么预处理时间为O(n2)

    查询耗时O(1), 但是需要额外的O(n2)空间,当数据量很大时,这个空间消耗是庞大的

    而且当改变了数组中的某一个值时,更新二维数组中的最小值也很麻烦。

    这时我们可以用到线段树来解决这个问题

    预处理耗时O(n),查询、更新操作O(logn),需要额外的空间O(n)。根据这个问题我们构造如下的二叉树

    • 叶子节点是原始组数arr中的元素
    • 非叶子节点代表它的所有子孙叶子节点所在区间的最小值

    由于线段树的父节点区间是平均分割到左右子树,因此线段树是完全二叉树

    对于包含n个叶子节点的完全二叉树,它一定有n-1个非叶节点,总共2n-1个节点

    因此存储线段是需要的空间复杂度是O(n)。  

    线段树就是分块思想的树化,或者说是对于信息处理的二进制化——用于达到O(logn)级别的处理速度,

    log2为底。(其实以几为底都只不过是个常数,可忽略)

    而分块的思想,则是可以用一句话总结为:

    通过将整个序列分为有穷个小块,对于要查询的一段区间,

    总是可以整合成k个所分块与m个单个元素的信息的并(0<=k,m<=sqrt{n})(0<=k,m<=n)

    但普通的分块不能高效率地解决很多问题,所以作为log级别的数据结构,线段树应运而生。

    神奇例题:洛谷P3372

    传送门

    1、建树与维护

    由于二叉树的自身特性,对于每个父亲节点的编号ii,他的两个儿子的编号分别是2i2i和2i+12i+1,所以我们考虑写两个O(1)O(1)的取儿子函数:

        int n;
        int ans[MAXN*4];
    
        inline int ls(int p){return p<<1;}//左儿子 
        inline int rs(int p){return p<<1|1;}//右儿子 

    1、此处的inlineinline可以有效防止无需入栈的信息入栈,节省时间和空间。

    2、二进制位左移一位代表着数值*22,而如果左移完之后再或上11,由于左移完之后最后一位二进制位上一定会是00,所以|11等价于+1+1。

    用二进制运算不是为了装X会变快的!

    那么根据线段树的服务对象,可以得到线段树的维护:

     void push_up_sum(int p){
            t[p]=t[lc(p)]+t[rc(p)];
        }// 向上不断维护区间操作 
    
        void push_up_min(int p){//max and min
         t[p]=min(t[lc(p)],t[rc(p)]);
         //t[p]=max(t[lc(p)],t[rc(p)]);             
        }


    push uppushup操作的目的是为了维护父子节点之间的逻辑关系。

    当我们递归建树时,对于每一个节点我们都需要遍历一遍,

    并且电脑中的递归实际意义是先向底层递归,然后从底层向上回溯

    所以开始递归之后必然是先去整合子节点的信息,再向它们的祖先回溯整合之后的信息。

    那么对于建树,由于二叉树自身的父子节点之间的可传递关系,所以可以考虑递归建树,并且在建树的同时,我们应该维护父子节点的关系:

    void build(ll p,ll l,ll r)
    {
        if(l==r){ans[p]=a[l];return ;}
        //如果左右区间相同,那么必然是叶子节点啦,只有叶子节点是被真实赋值的
        ll mid=(l+r)>>1;
        build(ls(p),l,mid);
        build(rs(p),mid+1,r);
    //此处由于我们采用的是二叉树,所以对于整个结构来说,可以用二分来降低复杂度,否则树形结构则没有什么明显的优化
        push_up(p);
    //此处由于我们是要通过子节点来维护父亲节点,所以pushup的位置应当是在回溯时。
    } 

    2、区间修改

    为什么不讨论单点修改呢qwq?因为其实很显然,单点修改就是区间修改的一个子问题而已,即区间长度为11时进行的区间修改操作罢了

    那么对于区间操作,我们考虑引入一个名叫“lazy tag”(懒标记)的东西——之所以称其“lazy”,是因为原本区间修改需要通过先改变叶子节点的值,

    然后不断地向上递归修改祖先节点直至到达根节点,时间复杂度最高可以到达O(nlogn)的级别。

    但当我们引入了懒标记之后,区间更新的期望复杂度就降到了O(logn)的级别且甚至会更低.

    (1)首先先来从分块思想上解释如何区间修改:

    分块的思想是通过将整个序列分为有穷个小块,对于要查询的一段区间

    总是可以整合成kk个所分块与mm个单个元素的信息的并(0<=k,m<=logn)(0<=k,m<=logn)

    (小小修改了一下的上面的前言)

    那么我们可以反过来思考这个问题:

    对于一个要修改的、长度为ll的区间来说,

    总是可以看做由一个长度为2log(floor{n})和剩下的元素(或者小区间组成)。

    那么我们就可以先将其拆分成线段树上节点所示的区间,之后分开处理:

    如果单个元素被包含就只改变自己,如果整个区间被包含就修改整个区间

    其实好像这个在分块里不是特别简单地实现,但是在线段树里,

    无论是元素还是区间都是线段树上的一个节点,所以我们不需要区分区间还是元素,加个判断就好。

    (2)懒标记的正确打开方式

    首先,懒标记的作用是记录每次、每个节点要更新的值,也就是delta,但线段树的优点不在于全记录(全记录依然很慢),而在于传递式记录:

    整个区间都被操作,记录在公共祖先节点上;只修改了一部分,那么就记录在这部分的公共祖先上;如果四环以内只修改了自己的话,那就只改变自己。

    m{After}Afte m{tha}tthat,如果我们采用上述的优化方式的话,我们就需要在每次区间的查询修改时pushdown一次,以免重复或者冲突或者爆炸

    那么对于pushdown而言,其实就是纯粹的pushup的逆向思维(但不是逆向操作): 因为修改信息存在父节点上,所以要由父节点向下传导lazy tag

    那么问题来了:怎么传导pushdown呢?这里很有意思,开始回溯时执行pushup,因为是向上传导信息;

    那我们如果要让它向下更新,就调整顺序,在向下递归的时候pushdown就可以了

    对于复杂度而言,由于完全二叉树的深度不超过logn,那么单点修改显然是O(logn)的,

    区间修改的话,由于我们的这个区间至多分logn个子区间,对于每个子区间的查询是O(1)的,所以复杂度自然是O(logn)

    inline void f(ll p,ll l,ll r,ll k)
    {
        tag[p]=tag[p]+k;
        ans[p]=ans[p]+k*(r-l+1);
        //由于是这个区间统一改变,所以ans数组要加元素个数次啦 
    }
    //我们可以认识到,f函数的唯一目的,就是记录当前节点所代表的区间 
    inline void push_down(ll p,ll l,ll r)
    {
        ll mid=(l+r)>>1;
        f(ls(p),l,mid,tag[p]);
        f(rs(p),mid+1,r,tag[p]);
        tag[p]=0;
        //每次更新两个儿子节点。以此不断向下传递 
    }
    inline void update(ll nl,ll nr,ll l,ll r,ll p,ll k)
    {
        //nl,nr为要修改的区间
        //l,r,p为当前节点所存储的区间以及节点的编号 
        if(nl<=l&&r<=nr)
        {
            ans[p]+=k*(r-l+1);
            tag[p]+=k;
            return ;
        }
        push_down(p,l,r);
        //回溯之前(也可以说是下一次递归之前,因为没有递归就没有回溯) 
        //由于是在回溯之前不断向下传递,所以自然每个节点都可以更新到 
        ll mid=(l+r)>>1;
        if(nl<=mid)update(nl,nr,l,mid,ls(p),k);
        if(nr>mid) update(nl,nr,mid+1,r,rs(p),k);
        push_up(p);
        //回溯之后 
    }

    3.对于区间查询

    ll query(ll q_x,ll q_y,ll l,ll r,ll p)
    {
        ll res=0;
        if(q_x<=l&&r<=q_y)return ans[p];
        ll mid=(l+r)>>1;
        push_down(p,l,r);
        if(q_x<=mid)res+=query(q_x,q_y,l,mid,ls(p));
        if(q_y>mid) res+=query(q_x,q_y,mid+1,r,rs(p));
        return res;
    }

    代码:

    #include<iostream>
    #include<cstdio>
    #define MAXN 1000001
    #define ll long long
    using namespace std;
    unsigned ll n,m,a[MAXN],ans[MAXN<<2],tag[MAXN<<2];
    inline ll ls(ll x)
    {
        return x<<1;
    }
    inline ll rs(ll x)
    {
        return x<<1|1;
    }
    void scan()
    {
        cin>>n>>m;
        for(ll i=1;i<=n;i++)
        scanf("%lld",&a[i]);
    }
    inline void push_up(ll p)
    {
        ans[p]=ans[ls(p)]+ans[rs(p)];
    }
    void build(ll p,ll l,ll r)
    {
        tag[p]=0;
        if(l==r){ans[p]=a[l];return ;}
        ll mid=(l+r)>>1;
        build(ls(p),l,mid);
        build(rs(p),mid+1,r);
        push_up(p);
    } 
    inline void f(ll p,ll l,ll r,ll k)
    {
        tag[p]=tag[p]+k;
        ans[p]=ans[p]+k*(r-l+1);
    }
    inline void push_down(ll p,ll l,ll r)
    {
        ll mid=(l+r)>>1;
        f(ls(p),l,mid,tag[p]);
        f(rs(p),mid+1,r,tag[p]);
        tag[p]=0;
    }
    inline void update(ll nl,ll nr,ll l,ll r,ll p,ll k)
    {
        if(nl<=l&&r<=nr)
        {
            ans[p]+=k*(r-l+1);
            tag[p]+=k;
            return ;
        }
        push_down(p,l,r);
        ll mid=(l+r)>>1;
        if(nl<=mid)update(nl,nr,l,mid,ls(p),k);
        if(nr>mid) update(nl,nr,mid+1,r,rs(p),k);
        push_up(p);
    }
    ll query(ll q_x,ll q_y,ll l,ll r,ll p)
    {
        ll res=0;
        if(q_x<=l&&r<=q_y)return ans[p];
        ll mid=(l+r)>>1;
        push_down(p,l,r);
        if(q_x<=mid)res+=query(q_x,q_y,l,mid,ls(p));
        if(q_y>mid) res+=query(q_x,q_y,mid+1,r,rs(p));
        return res;
    }
    int main()
    {
        ll a1,b,c,d,e,f;
        scan();
        build(1,1,n);
        while(m--)
        {
            scanf("%lld",&a1);
            switch(a1)
            {
                case 1:{
                    scanf("%lld%lld%lld",&b,&c,&d);
                    update(b,c,1,n,1,d);
                    break;
                }
                case 2:{
                    scanf("%lld%lld",&e,&f);
                    printf("%lld
    ",query(e,f,1,n,1));
                    break;
                }
            }
        }
        return 0;
    }

     

  • 相关阅读:
    ie6不支持label
    IE6下li会继承ul属性的bug、产生条件、解决办法
    玉树地震与汶川地震
    IE6给png图片添加透明级别
    使用Float布局容器高度出错的决办法
    CSS冒泡窗口,有机会改成js的
    沁园春《房》
    乱接电话的笑话~
    禁止使用英文及其缩写?
    jQuery
  • 原文地址:https://www.cnblogs.com/U58223-luogu/p/9561217.html
Copyright © 2011-2022 走看看