1.1 线段树的基础操作

本篇对应的是luogu的线段树1

概况:

如下图就是一棵线段树,线段树上的每一个点记录的都是一个区间,所以线段树支持对于区间和点的动态操作,可以在线查询和更改区间上的最值,求和等

时间复杂度:O(n)

 

          1.1 线段树的基础操作_第1张图片

 

 

使用线段树的情况

  满足区间加法:已知左右两子树的全部信息,一定能够推出父节点

       线段树维护的内容根据题目的要求而定

线段树的分类

根据题目中对于查询和修改区间的不同要求,大致将线段树分为三类:

  problem1:单点修改,单点查询

  problem2:区间修改,单点查询

  problem3:区间修改,区间查询

思路:

思路+代码阅读(注:不同题目线段树维护的内容不同,这里给出区间和写法,如果要维护其他内容,只需把下面所有提及sum的内容更改为新的转移方程即可):

001:建立线段树  利用二分,不断维护最终要求的值,返回条件为找到叶节点O(n)

      //构造一棵a1-an的线段树  调用build_xdtree(1,1,a)

      void build_xdtree(int o,int l,int r){

      //o是当前这段区间的点编号, sum[o]用于记录区间和,l是左节点,r是右节点

             if(r==l){     //如果已经找到叶节点,它的区间和就是它自己的值

                    sum[o]=a[l];   //a[l]用于记录每个点本身的值

                    return;   //找到叶节点,返回

             }

             int mid=(l+r)>>1;   //从中间二分

             build_xdtree(o<<1,l,mi);  //分别build它的左右节点

             build_xdtree(o<<1+1,mid+1,r);

             sum[o]=sum[o<<1]+sum[o<<1+1]; //它的区间和即左右区间和的和

      }

002:PROB1 单点修改和单点查询

    单点修改:O(log2(n))

从根节点开始找,判断x在左右子树

最后更新被影响到的值(和建树相同),条件为叶节点(x)

      //线段树点的更新 如果更新a[x]=y

      void uponedate(into,intl,intr,intx,int y){

             if(l==r){            //找到根节点,也就是x

                    sum[o]=y;//更新根节点为新的y

                    return;         //返回

             }

             int mid=(l+r)>>2;

             if(x<=mid){                   //如果x在o的左子树里

                    update(o<<2,l,mid,x,y);   //在左子树里继续找,右子树的值不受影响

             }

             else update(O<<2+1,mid+1,r,x,y);

      //反之在右节点里更新 左节点的值不受影响

             sum[o]=sum[o<<2]+sum[o<<2+1];

      //更新sum[o]的值为更新过后的左右子树之和

      }

  单点查询O(log2(n))

从根节点开始找,终止条件为覆盖区间,分别判断左右子树有没有包含区间一部分

 

      //在只修改点情况下线段树的查询区间ax-ay ans用于记录结果

      void onequery(int o,int l,int r,int x,int y){

             if(x<=l&&y>=r){//思考:为什么这样写

                    ans+=sum[o];//对于覆盖区间[l,r]的[x,y],ans直接加上[l,r]的sum

                    return;//实际上就等同于找到了完全相等的区间,可以不用向下搜了

             }

             int mid=(l+r)>>1;

             if(x<=mid){      //如果有一部分在[l,r]的左子树里,就在左子树里搜索

                    query(o<<1,l,mid,x,y)

             }

             if(y>mid){        //如果有一部分在[l,r]的右子树里,就在右子树里搜索

                    query(O<<1+1,mid+1,r,x,y);

             }

      }

003:lazy_tag(重点)

对于区间的修改,可以定义一个lazytag,给这个区间打上标记来代替直接对它进行操作,那么在读取或进行其他操作的时候就可以一起读出lazytag,优化了时间复杂度,注意:假设a[o]有一个大小为k的tag,意味着它的子节点都欠着一个tagk没有加。但是a[o]不欠,它的值在调用更改函数时(或者由它的父亲传下来时),就已经更新过了。所以对于a[o]而言,决定它大小的是它的父亲的tag。

push up: 读完tag之后,所有的儿子会更新它的数值(可能是最值、区间和等),那么他们的父亲也会随之更新数值,所以在所有的递归改变了儿子之后,都要调用pushup来更新父亲

      // 如果一个子节点加了x,那么它的父节点也要加上x 在每次递归完左右儿子后调用

      void pushup(int o){

             sum[o]=sum[o<<2]+sum[o<<2+1];

      }

       push down:父亲节点的tag会对子节点的数值造成影响,所以在所有的递归儿子之前,为了准确计算儿子的值,都要把父亲的tag传递给它的子节点,并更新子节点的值,同时可以清除父亲的tag,因为他的子节点更新了

      //  将父节点的tag进行下传 在每次递归左右儿子前调用

      void pushdown(int o,int l,int,r,int mid){

             if(tag[o]){//如果父节点的tag不为0

                    tag[o<<1+1]+=tag[o];//子节点可能会多次更新tag

                    tag[o<<1]+=tag[o];//下传 为了不影响tag的添加

                    sum[o<<1]+=tag[o]*(mid-l+1);//节点长度

                    sum[o<<1+1]+=tag[o]*(r-mid+1); //更新sum的值

                    tag[o]=0;//父节点的tag都下传了 所以更新为0

             }

      }

004:PROB2 区间修改和区间查询

区间修改:

从根节点开始,找到覆盖就打tag,更新值,返回

下传tag,分别判断左右子树有没有包含区间一部分来递归,更新父节点

      //线段树的区间修改 修改内容为ax—ay都加v

      void update(int o,int l,int r,int x,int y,int v){//当前搜索到的点

             if(x<=l&&y>=r){//如果[x,y]覆盖了当前找到的[l,r]

                    tag[o]+=v;//给区间[l,r]打上tag,那么就不用再找他的孩子

                    sum[o]+=(r-l+1)*tag[o];//前面说过,此时要更新sum[o]的值

                    return ;//不用找它的孩子,直接返回

             }

             else{  //开始遍历左右子树

                    pushdown(o,l,r,(l+r)>>1);

      //遍历前先下传tag,如果不传,它的子节点就还欠债,计算的值就有错误

                    int mid=(l+r)>>1;

                    if(x<=mid){  //如果左子树里有[x,y]的一部分

                           update(o<<1,l,mid,x,y,v);

                    }

                    if(y>mid){  //如果右子树里有[x,y]的一部分

                           update(o<<1+1,mid+1,r,x,y,v)

                    }

                    pushup(o);//在搜完左右节点以后,更新一下当前节点的信息

             }

      }

区间查询:

      //线段树的查询区间ax-ay 直接返回 

      int query(int o,int l,int r,int x,int y){

             if(x<=l&&y>=r){//如果[x,y]覆盖了[l,r]区间,[l,r]区间的值就是答案的一部分

                    return sum[o];//因为是int型,直接返回[l,r]的值,不需要向下搜了

             }

             else{

                    pushdown(o,l,r,(l+r)/2);//涉及到子节点的值,先打tag

                    int mid=(l+r)>>1;

                    int ans=0;      //记录答案

                    if(x<=mid){    //如果有一部分在[l,r]的左子树里

                           ans+=query(o<<1,l,mid,x,y)

                    }

                    if(y>mid){     //如果有一部分在[l,r]的右子树里

                           ans+=query(o<<1+1,mid+1,r,x,y);

                    }

                    return ans;      //最后不需要更新父节点,因为没有改变子节点的大小

             }

      }

005 整理

         思考:

                001 线段树的结构,可以解决的问题类型,时间复杂度

                002 如何建立一棵线段树呢

                003 在写所有关于点的函数时,他们有什么共同点和不同点

                       比如:定义内容,终止条件,更新内容,分类标准,更不更新父节点

                004 在写所有关于区间的函数时,他们有什么共同点和不同点

                       比如:定义内容,终止条件,更新内容,分类标准,下传tag,更不更新父节点

    回忆上面内容中标红(细节)和下划线的地方(结构),尝试还原算法

 

                 ---END---

 

第一版:2019-10-02

 

你可能感兴趣的:(1.1 线段树的基础操作)