segment tree 详解

综述

  线段树是一颗二叉搜索树,树的每一个节点均维护着区间信息,线段树根节点的区间信息可通过左子树和右子树的信息计算出(也就是我们说的满足区间加法)。

常见的几种区间加法:总的数字之和=左区间的数字和+右区间的数字和

          总的gcd=gcd(左,右)

          总的数字乘积=左区间乘积*右区间乘积

          总的最大值=max(左区间最大值,右区间最大值)//最小值同理

  它有什么用呢?它可以解决区间问题,可以在log(n)的时间进行单点查询,区间查询,单点修改等操作,例如题目给你一个长度为n的序列,要求进行k次操作,每次操作可能改动一个点的值,也可以查询区间L到R的和,如果单有查询那么可以用前缀和,加上修改前缀和就不行了,如果暴力做那么复杂度在查询就是O(nk)了,但如果用线段树可以优化到O(klogn)。

它长什么样?怎么存?

  对于一颗维护长度为8的线段树,它的理念形态是长这样的:

             

  总区间自然是1到8,那么左区间为什么是1到4右区间是5到8呢,因为线段树的核心是二分性质,区间【L,R】的左区间为【L,m】右区间为【m+1,R】,m为(L+R)/2。需注意端点m是属于左区间的。

  那么如果赋予它实际的值,告诉你8个数字分别为1 2 3 4 5 6 7 8,那么它的实际情况是长这样的:

              

  如果给它标号,每一层从上到下,从左到右由小到大进行标号,可以看得出来,根节点为n号时,左子树为2*n号,右子树为2*n+1号:

               

   那怎么存呢?用数组来存,a[i]代表标号为i的节点信息,那么数组开多大呢?,数组的空间应该为节点的个数+1,那对于长度为n的线段树,节点有多少个呢?

   当n是2的幂时,那么线段树就是满二叉树,它的层数是log2(n)+1,那么节点数就是2层数-1也就是2n-1,当n不是2的幂的时候,线段树就会不那么好看,叶子节点的数组空间就不一定是连续的:

例如这个n=10的线段树:

  

   可以看到它不是满二叉数,满二叉树的n要达到16,若按照上面的计算公式开2n-1的空间数也就是19而已,而实际最大下标去到了25,不能满足,所以我们得开大一点,对于一个不是2的幂的n来说,我们令开多一层空间,就是是说n=10我们就开n=16的空间 , n=17我们就开n=32的空间,log2(n)+1是对于是2的幂的n的层数,为了方便无论n是不是2的幂,我们都开多一层空间给它 , 所以层数=log2(n)+1+1 , 总节点数开到2log2(n)+1+1-1,也就是4n-1,所以开4倍就行,证毕!

普通线段树代码详解

  要开的变量:maxn是n的最大范围,A[maxn]存题目的n个值的信息,对应线段树的叶子节点    a[4*maxn]是线段树的空间,节点的标号为i。

对于节点信息的维护

  注意节点存的是什么信息,此代码存的是区间和:

void pushup(int rt){
    a[rt]=a[rt<<1]+a[rt<<1|1];
}

建树

  建树的过程就是一直递归到叶子节点,然后把当前编号的空间赋值为对应题目的信息,在递归结束的时候要维护区间信息

 1 void build(int l,int r,int rt){//对于 l 到 r 的一颗线段树建树 
 2     if(l==r){//代表已经递归到叶子节点 
 3         a[rt]=A[l];
 4         return ;
 5     }
 6     int m=l+r>>1;
 7     build(l,m,rt<<1);//建左子树 
 8     build(m+1,r,rt<<1|1);//建右子树 
 9     pushup(rt);//左右子树建好之后维护当前区间的信息 
10 }

 单点更新

  单点更新其实就是一个找叶子节点编号的二分过程,当你找到对应叶子节点的位置,你想干嘛干嘛,这里是对应位置加上val值:

 1 void update(int pos,int val,int l,int r,int rt){//在pos位置加上val
 2     if(l==r){//代表已经递归到叶子节点 
 3         a[rt]+=val;
 4         return;
 5     }
 6     int m=l+r>>1;
 7     if(pos<=m)//目标位置在左边 
 8         update(pos,val,l,m,rt<<1);
 9     else//在右边 
10         update(pos,val,m+1,r,rt<<1|1);
11     pushup(rt);//更新完维护信息 
12 }

单点查询

  单点查询其实和单点更新本质上相同,就是找到叶子节点,然后想干嘛干嘛

int search(int pos,int l,int r,int rt){
    if(l==r){
        return a[rt];
    }
    int m=l+r>>1;
    if(pos<=m)
        return search(pos,l,m,rt<<1);
    else
        return search(pos,m+1,r,rt<<1|1);
}

区间查询

  在区间查询中,有两个区间,一个是查询区间我们设为【L,R】,一个是递归区间设为【l,r】。假如我们要查询区间和,设为ans。

 1 int query(int L,int R,int l,int r,int rt){
 2     if(L<=l&&r<=R){
 3         return a[rt];//第一种情况,直接贡献 
 4     }
 5     int m=l+r>>1;
 6     int ans=0;
 7     if(L<=m)//递归区间的左边含有查询 
 8         ans+=query(L,R,l,m,rt<<1);
 9     if(R>m)
10         ans+=query(L,R,m+1,r,rt<<1|1);
11     return ans;//统计二三情况的贡献,返回最终值 
12 }

  分三种情况:

  一是递归区间属于查询区间,L<=l&&r<=R,就是说本节点所表示的信息你全部都要,那就贡献到ans里。

  二是递归区间的左边有查询区间,L<=m,但本节点信息你不是全部要,得递归深一点,直到满足情况一才贡献ans,遂令下一个递归区间为【l,m】。

  三是递归区间的右边有查询区间,R>m,但本节点信息你不是全部要,得递归深一点,直到满足情况一才贡献ans,遂令下一个递归区间为【R,m+1】。

             

  

 区间更新

  假如我们要将区间【L,R】中的每一个端点加上一个值val,如果区间更新用R-L+1个单点更新来做,那么复杂度要去到O(长度*logn),那么最坏长度为整个序列那么长,就是nlogn了。为了降低时间复杂度,我们引入懒标记这一概念。

  每一个节点都有一个懒标记值,懒标记——表示本节点的左右子树有区间更新需求但尚未更新。懒标记顾名思义就是太懒了,懒得递归下去,打个标记,之后如果要用到就顺便更新了。

  

  举个例子,对于n=10,各个端点值为1 2 3 4 5 6 7 8 9 10,

 

 

猜你喜欢

转载自www.cnblogs.com/qq2210446939/p/12215363.html