• 线段树模板


    线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点,每个节点所储存的是一个区间的信息。

    假设有编号从1到n的n个点,每个点都存了一些信息,用[L,R]表示下标从L到R的这些点。
    线段树的用处就是,对编号连续的一些点进行修改或者统计操作,修改和统计的复杂度都是O(log2(n)).

    线段树的原理,就是,将[1,n]分解成若干特定的子区间(数量不超过4*n),然后,将每个区间[L,R]都分解为
    少量特定的子区间,通过对这些少量子区间的修改或者统计,来实现快速对[L,R]的修改或者统计。

    一.基本概念及其形状

    1.使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,实际应用时一般还要开4N的数组以免越界,因此有时需要离散化让空间压缩。

    2.每个区间包括以下几个部分:
    (1)区间左端点,右端点(必须有)
    (2)根据题目信息来实现维护修改之类的(根据实际情况而定)

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

    4.线段树的一般结构:

    由上图可以看出每个叶子节点的区间都是[L,mid],[mid+1,R],L代表上一个区间的左端点,R代表上一个区间的右端点,mid代表上面左右区间的中间值。所以线段树是很有规律的。

    二.解决题目的基本操作

    1.首先一般求解树的问题,都是先要建树。线段树也不例外,建树一般都是模板的。如下:

    const int maxn=10000; //一般为点的总个数 
    int a[maxn+5],st[maxn+5];
    int sum[maxn*4+1];
    void pushup(int rt){
        sum[rt]=sum[rt*2]+sum[rt*2+1];  //这里是求和的pushup,如果求max就改成sum[rt]=max(sum[rt*2],sum[rt*2+1]这个样子。
    } //用来把当前节点信息更新到父节点 (求和更新) 
    
    void build(int l,int r,int rt){  //rt表示当前子树的根,也就是当前所在的节点 
        if(l==r){     //如果左端点等于右端点,则说明到了底端叶子节点则返回 
            st[rt]=a[l];
            return ; 
        }
        int m=(l+r)/2;
        build(l,m,rt*2);  //构建左子树 
        build(m+1,r,rt*2+1); //构建右子树 
        pushup(rt); 
    } 

     2.建好树了后对于题目就会有一系列的要求,比如单点替换(更新),求区间最值,求区间和之类的

    点修改:

    void update(int l,int r,int p,int add,int rt){ // add为要增加的数,p代表判断要更新的数字, 
        if(l==r){
            sum[rt]+=add;  //如果是叶节点则修改 
            return ;
        }
        int m=(l+r)/2;
        if(p<=m) update(l,m,p,add,rt*2); //左边l,右边m 
        else update(m+1,r,p,add,rt*2+1); //左边m+1,右边 r 
        pushup(rt); 
    }

    区间求和:

    int query(int L,int R,int l,int r int rt){ //[L,R]表示操作区间,[l,r]表示总区间,rt表示当前节点编号 
        if(L<=l && R>=r){ //在区间内直接返回 
            return sum[rt];
        }
        int m=(l+r)/2;  //左子区间:[L,m] 右子区间:[m+1,R] 求和区间:[l,r] 
        int ans;
        if(L<=m) ans+=query(L,R,l,m,rt*2); //左子区间与[l,r]有重叠,递归 
        if(R>m)  ans+=query(L,R,m+1,r,rt*2+1); //右子区间与[l,r]有重叠,递归 
        return ans;
    }

     区间求最大值:

    int query(int L,int R,int l,int r,int rt){//L,R查询的区间,l,r总区间
        if(L<=l&&R>=r){
            return Max[rt];
        }
        int ans=0;
        int m=(l+r)/2;
        if(L<=m) ans=max(ans,query(L,R,l,m,ls));
        if(R>m) ans=max(ans,query(L,R,m+1,r,rs));
        return ans;
    }

    三.区间方面的线段树

    在做区间修改的时候,有个叫懒惰标记(延迟标记)。

    根据大佬们的解释,标记的含义:
    代表了这个结点的值已经被更新过了, 但是没有进行子树的结点值更改操作, 用lazy数组标记一下.
    所以, 每次进行值的更新和查询操作, 每到一个有 lazy 标记的结点, 必须进行向下更新.

    即,如果要给一个区间的所有值都加上1,那么,实际上并没有给这个区间的所有值都加上1,而是打个标记,记下来,这个节点所包含的区间需要加1.打上标记后,要根据标记更新本节点的统计信息,比如,如果本节点维护的是区间和,而本节点包含5个数,那么,打上+1的标记之后,要给本节点维护的和+5。这是向下延迟修改,但是向上显示的信息是修改以后的信息,所以查询的时候可以得到正确的结果。有的标记之间会相互影响,所以比较简单的做法是,每递归到一个区间,首先下推标记(若本节点有标记,就下推标记),然后再打上新的标记,这样仍然每个区间操作的复杂度是O(log2(n))。

    标记有相对标记和绝对标记之分:
    相对标记是将区间的所有数+a之类的操作,标记之间可以共存,跟打标记的顺序无关(跟顺序无关才是重点)。
    所以,可以在区间修改的时候不下推标记,留到查询的时候再下推。
          注意:如果区间修改时不下推标记,那么PushUp函数中,必须考虑本节点的标记。
                     而如果所有操作都下推标记,那么PushUp函数可以不考虑本节点的标记,因为本节点的标记一定已经被下推了(也就是对本节点无效了)
    绝对标记是将区间的所有数变成a之类的操作,打标记的顺序直接影响结果,
    所以这种标记在区间修改的时候必须下推旧标记,不然会出错。

    注意,有多个标记的时候,标记下推的顺序也很重要,错误的下推顺序可能会导致错误。

    之所以要区分两种标记,是因为非递归线段树只能维护相对标记。
    因为非递归线段树是自底向上直接修改分成的每个子区间,所以根本做不到在区间修改的时候下推标记。
    非递归线段树一般不下推标记,而是自下而上求答案的过程中,根据标记更新答案。

    所以来说,做普通的线段树单点更新区间查询的时候用不到这个懒惰标记,而做区间更新修改的时候就要加点东西了,而且上面的有些函数也需要加一点东西了。

    而且看大佬们一般都用位运算好想说位运算比普通的乘除快,所以基本的先熟悉一下,rt<<1代表位左移一位,就是乘2,rt>>1代表位右移一位,就是除2, rt | 1 就代表加1。

    区间更新与点更新不同,它需要更深层次的理解,最为基本的Add(L, R, V)与Query(L,R)系列,然后以此为基础可以衍射出其他各种做法:例如Set(L, R, V)系列就是其衍射品,Set系列更改的就是在查询的时候读取到Lazy标记就立刻返回,而Add系列不同,他要一直往下读它的Lazy标记。

     

    首先是pushdown的模板:

    void push_down(ll m,ll rt){ //m为总区间长度
        if(lazy[rt]){
            lazy[ls]+=lazy[rt];
            lazy[rs]+=lazy[rt];
            sum[ls]+=lazy[rt]*(m-(m>>1));
            sum[rs]+=lazy[rt]*(m>>1);
            lazy[rt]=0;
        }
    }

    最后是区间更新,更新从根向下,若是(L,R)包含了这个根所在的区间,就直接给这个区间附上lazy标记并且返回,否则就说明有冗余的东西,就得把目前这个点的lazy也下推,然后在新的区间继续找新的值。

    void update(ll L,ll R,ll c,ll l,ll r,ll rt){//区间更新[l,r],[L,R]代表总区间
        if(l<=L&&r>=R){
            lazy[rt]+=c;
            sum[rt]+=c*(R-L+1);
            return ;
        }
        push_down(R-L+1,rt); //所有的节点都更新一遍lazy标记
        ll m=(L+R)>>1; //总区间的一半
        if(l<=m) update(L,m,c,l,r,ls); //注意这里递归的是总区间为变量
        if(r>m) update(m+1,R,c,l,r,rs);
        push_up(rt);
    }

     经常自己翻自己博客发现还是得给自己加个板子来结合。

    所以贴一个板子题:

    hdu1166

     

    #include<stdio.h>
    const int maxn=50000;
    int sum[maxn*4+1];
    void pushup(int rt){
        sum[rt]=sum[rt*2]+sum[rt*2+1];
    }
    
    void build(int l,int r,int rt){
        if(l==r){
            scanf("%d",&sum[rt]);
            return ;
        }
        int m=(l+r)/2;
        build(l,m,rt*2);
        build(m+1,r,rt*2+1);
        pushup(rt);
    }
    
    void update(int l,int r,int p,int add,int rt){
        if(l==r){
            sum[rt]+=add;
            return ;
        }
        int m=(r+l)/2;
        if(p<=m) update(l,m,p,add,rt*2);
        else update(m+1,r,p,add,rt*2+1);
        pushup(rt);
    }
    
    int  query(int L,int R,int l,int r,int rt){
        if(l<=L&&r>=R)    return sum[rt];
        int ans=0;
        int m=(L+R)/2;
        if(l<=m) ans+=query(L,m,l,r,rt*2);
        if(r>m)  ans+=query(m+1,R,l,r,rt*2+1);
        return ans;
    }
    
    int main()
    {
        int t;
        char d[10];
        scanf("%d",&t);
        for(int a=1;a<=t;a++){
            printf("Case %d:
    ", a);
            int n;
            scanf("%d",&n);
            build(1,n,1);
            while(scanf("%s",d)!=EOF){
                if(d[0]=='E') break;
                int x,y; //x营地,y人 或 x,y区间 
                scanf("%d%d",&x,&y);
                if(d[0]=='Q') {
                    int h=query(1,n,x,y,1);
                    printf("%d
    ",h); 
                }
                if(d[0]=='U') update(1,n,x,y,1);
                if(d[0]=='S') update(1,n,x,-y,1);
            }
        }
        return 0;
    }

     

     

  • 相关阅读:
    JAVA_SE基础——47.接口
    抽象类和接口的区别[精华版]
    JAVA_SE基础——46.引用数据类型变量.值交换[独家深入解析]
    JAVA_SE基础——45.基本类型变量.值交换[独家深入解析]
    第一个Spring程序
    三层架构和MVC的区别
    Spring 概述及IOC理论推导
    Mybatis之缓存
    Mybatis之动态SQL
    Mybatis之一对多和多对一处理
  • 原文地址:https://www.cnblogs.com/wushengyang/p/10293130.html
Copyright © 2020-2023  润新知