链接来源:https://blog.csdn.net/zearot/article/details/52280189


                                   线段树从零开始

一:为什么需要线段树?

 题目一:

10000个正整数,编号从1到10000,用A[1],A[2]……A[10000]表示。

修改:无

统计:编号从L到R的所有数之和为多少?其中1<=L<=R<=10000.

方法一:对于统计L,R,需要求下标从L到R的所有数的和,从L到R的所有下标记作[L……R],

问题就是对A[l……R]进行求和。

方法二:更快的方法是 求前缀和,令S[0]=0,S[k]=A[1……k],那么,A[L……R]的和就等于S[R]-S[L-1],

这样,对于每个询问,就只做一次减法,大大提高效率。

 

题目二:

10000个正整数,编号从1到10000,用A[1],A[2]……A[10000]表示。

修改:将第L个数增加C(1<=L<=10000)

统计:编号从L到R的所有数之和为多少?

其中1<=L<=R<=10000

 

 

 再使用方法二的话,假如A[L]+=C之后,S[L],S[L+1]……S[R]都需要增加C,全部都要修改,

见下表:

  方法一 方法二
A[L]+=C 修改一个元素 修改R-L+1个元素
求和A[L……R] 计算R-L+1个元素之和 计算两个元素之差

 从上表可以看出,方法一修改快,求和慢。方法二求和快,修改慢。

那有没有一种结构,修改和求和都比较快那?  线段树

 

二:线段树点的修改

 

上面的问题二就是典型的线段树点的修改。

线段树先将区间[1……10000]分成不超过4*10000个子区间,对于每个子区间,记录一段连续数字的和。

之后,任意给定区间[L,R],线段树在上述子区间中选择约2*log(R-L+1)个拼成区间[L,R]。的子区间中,约有

如果A[L]+=C,线段树的子区间中,约有log2(10000)个包含了L,所以需要修改log2(10000)个。

 

使用线段树的话,A[L]+=C需要修改log2(10000)个元素。求和A[L……R]需要修改2*log2(R-L+1)<=2*log2(10000)个元素。

log2(10000)<14所以相对来说,线段树的修改和求和都比较快。

 

问题一:开始的子区间是怎么分的?

首先,讲解原始子区间的分解。

 假定给定区间[L,R],只要L<R,线段树就会把它继续分裂成两个区间。首先计算M=(L+R)/2,左子区间为[L,M],右子区间为[M+1,R],然后如果子区间不满足条件就递归分解

以区间[1……13]的分解为例,分解结果见下图:

线段树入门知识

问题二:给定区间[L,R],如何分解成上述给定的区间? 

    对于给定区间[2,12]要如何分解成上述空间那?

分解方法一:自下向上合并——利于理解

先考虑树的最下层,将所有在区间[2,12]内的点选中。然后,若与相邻点的结点为同一父节点,那么就用这个父节点代替这两个结点(父节点在上一层)。这样操作之后,本层最多剩下两个节点。若最左侧被选中的节点是他父节点的右子树,那么这节点会被剩下。若最右侧被选中的节点是他父节点的左子树,那么这个节点也会被剩下。中间所有的节点都被父节点取代。

对最下层处理完之后,考虑它的上一层,继续进行同样的处理。

下图为n=13的线段树,区间[2,12],按照上面的叙述进行操作的过程图:

   线段树入门知识

线段树入门知识

 线段树入门知识

 线段树入门知识

由图可以看出:n=13的线段树中 ,2,12]=[2] + [3,4] + [5,7] + [8,10] + [11,12] 。

 

分解方法二:自上而下分解——利于计算

线段树入门知识

首先,对于区间[1,13],计算(1+13)/2=7,于是将区间[2,12]切割成了[2,7]和[8,12]。

 线段树入门知识

其中,[2,7]处于节点[1,7]的位置,[2,7]<[1,7]所以继续分解,计算(1+7)/2=4,于是将[2,7]切割成 [2,4]和[5,7]。

线段树入门知识

[5,7]处于结点[5,7]的位置,所以不用继续分解。[2,4]处于区间[1,4]的位置,所以接续分解成[2]和[3,4] .

线段树入门知识

最后,[2]<[1,2],所以计算(1+2)/2=1,将[2]用1切割,左侧为空,右侧 为[2]。

线段树入门知识

上图只表示计算方法,不代表计算顺序。 因为程序是递归计算的,而不是一层一层计算。

 

问题三:如何计算区间统计?

假设这13个数为1,2,3,4,1,2,3,4,1,2,3,4,1。在区间之后标上该区间的数字之和:

线段树入门知识

 如果要计算[,12]的和,按照之前的算法:

[2,12]=[2]+[3,4]+[5,7]+[8,10]+[11,12]

29=2+7+6+7+7

计算5个数的和就可以算出[2,12]的值。

 

问题四:如何进行点修改?

假设把A[6]+=7,看看那些区间需要进行修改?[6],[5,6],[5,7],[1,7],[1,13]这些区间全部都需要+7,其余所有的区间都不需要动。

于是,这颗线段树中,点修改最多修改5个线段树元素(每层一个)。

下图中,修改后的元素用蓝色表示。

线段树入门知识

问题五:存储结构是怎样的? 

线段树是一种二叉树,当然可以像一般的树那样写成结构体,指针什么的。但是它的优点是,它也可以用数组来实现树形结构,可以大大简化代码。

数组形式适合在编程竞赛中使用,在已经知道线段树的最大规模的情况下,直接开足够空间的数组,然后在上面建立线段树。

简单的记法:足够的空间=数组大小n的四倍

实际上足够的空间=(n向上扩充到最近的2的某个次方)的两倍。

举例子:假设数组长度为5,就需要5先扩充成8,8*2=16。线段树需要16个元素。如果数组元素为8,那么也需要16个元素。

所以线段树需要的空间是n的两倍到四倍之间的某个数,一般就开4*n的空间就好,如果空间不够,可以自己算好最大值来省点空间。

 

怎么用数组来表示一棵二叉树那?假设某个节点的编号为pos,那么它的左子节点编号为2*pos,右子节点编号为2*pos+1。然后规定根节点为1,这样一颗二叉树就构造完成了。通常2*pos在代码中写成pos<<1。2*pos+1写成pos<<1|1

 

问题六:代码中如何实现?

(1)定义:

#define maxn 100007//元素总个数
int sum[maxn<<2];//sum求和,开四倍空间
int A[maxn],n;//存原数组下标[1,n]
#define lson pos<<1  //代表左子树 
#define rson pos<<1|1 //代表右子树

(2)建树:

void build(int pos,int l,int r)
{
    if(l==r)//到达叶子节点
    {
      
       sum[pos]=A[l];//存储A数组的值
       return;//一定要有这个return
     }
    
     int mid=(l+r)/2;
    
     //左右递归
     build(lson,l,r);
     build(rson,l,r);

     sum[pos] = sum[lson] + sum[rson];
}

     

(3)点修改

假设A[L]+=C:

void update(int pos,int L,int C,int l,int r)//[l,r]表示当前区间,pos是当前节点编号
{
   if(l==r)//到达叶子节点,修改叶节点的值
   {
   
      sum[pos]+=C;
      return;

   }  
  
   int mid=(l+r)/2;
  
   //根据条件判断往左子树调用还是往右子树
    
   if(L<=mid) update(L,C,l,mid,lson);

     else  update(L,C,mid+1,r,rson);

    sum[pos]=sum[lson]+sum[rson];

}

点修改其实可以写的更简单,只需要把一路经过的sum都+=C就行了,不过上面的代码 更加规范,在题目更加复杂的时候,按照格式写更不容易出错。

 

(4)区间查询(本题为求和)

询问A[L……R]的和。

注意到,整个函数的递归过程中,L,R是不变的。

首先,如果当前区间[l,r]在[L,R]内部,就直接累加答案;如果左子区间与[L,R]有重叠就递归左子树,右子树同理。

int Query(int pos,int L,int R,int l,int r)
       //[L,R]表示操作区间,[l,r]表示当前区间,pos当前节点编号
{
   if(L>=l&&r>=R)//在区间内直接返回   [L,R]在[l,r]内部
   {
     
     return sum[pos];
   }
   
   int mid=(l+r)/2;
   //左子区间[l:mid],右子区间[mid+1,r],求和区间[L,R]
   //累加答案
   int ans=0;
   if(L<=mid)
       ans+=Query(lson,L,R,l,mid);//左子区间与[L,R]有重叠,递归
   if(R>mid)
       ans+=Query(rson,L,R,mid+1,r);//右子区间为与[L,R]有重叠,递归
   
   return ans;
}

 

 

 

 

 

 

 

 

 

 

相关文章:

  • 2021-07-25
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
猜你喜欢
  • 2021-08-20
  • 2021-06-08
  • 2021-07-14
  • 2021-05-19
相关资源
相似解决方案