线段树

来源:互联网 发布:vnr更新网络 编辑:程序博客网 时间:2024/06/05 02:13

转载地址:http://blog.csdn.net/zearot/article/details/52280189



         线段树



一:为什么需要线段树?
题目一:
10000个正整数,编号1到10000,用A[1],A[2],A[10000]表示。
修改:无
统计:1.编号从L到R的所有数之和为多少? 其中1<= L <= R <= 10000.

方法一:对于统计L,R ,需要求下标从L到R的所有数的和,从L到R的所有下标记做[L..R],问题就是对A[L..R]
进行求和。
这样求和,对于每个询问,需要将(R-L+1)个数相加。

方法二:更快的方法是求前缀和,令 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]表示。
修改:1.将第L个数增加C (1 <= L <= 10000)
统计:1.编号从L到R的所有数之和为多少? 其中1<= L <= R <= 10000.

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


方法一方法二A[L]+=C修改1个元素修改R-L+1个元素求和A[L..R]计算R-L+1个元素之和计算两个元素之差
从上表可以看出,方法一修改快,求和慢。 方法二求和快,修改慢。
那有没有一种结构,修改和求和都比较快呢?答案当然是线段树。


二:线段树的点修改

上面的问题二就是典型的线段树点修改。
线段树先将区间[1..10000]分成不超过4*10000个子区间,对于每个子区间,记录一段连续数字的和。
之后,任意给定区间[L,R],线段树在上述子区间中选择约2*log2(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. 在区间之后标上该区间的数字之和:

如果要计算[2,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个线段树元素(每层一个)。
下图中,修改后的元素用蓝色表示。

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

线段树是一种二叉树,当然可以像一般的树那样写成结构体,指针什么的。
但是它的优点是,它也可以用数组来实现树形结构,可以大大简化代码。
数组形式适合在编程竞赛中使用,在已经知道线段树的最大规模的情况下,直接开足够空间的数组,然后在上面建立线段树。
怎么用数组来表示一颗二叉树呢?假设某个节点的编号为v,那么它的左子节点编号为2*v,右子节点编号为2*v+1。
然后规定根节点为1.这样一颗二叉树就构造完成了。通常2*v在代码中写成 v<<1 。 2*v+1写成 v<<1|1 。

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

(0)定义:

[cpp] view plain copy
 
 print?
  1. #define maxn 100007  //元素总个数  
  2. int Sum[maxn<<2];//Sum求和  
  3. int A[maxn],n;//存原数组数据下标[1,n]   

(1)建树:

[cpp] view plain copy
 
 print?
  1. //PushUp函数更新节点信息 ,这里是求和  
  2. void PushUp(int rt){Sum[rt]=Sum[rt<<1]+Sum[rt<<1|1];}  
  3. //Build函数建树   
  4. void Build(int l,int r,int rt){ //l,r表示当前节点区间,rt表示当前节点编号  
  5.     if(l==r) {//若到达叶节点   
  6.         Sum[rt]=A[l];//储存数组值   
  7.         return;  
  8.     }  
  9.     int m=(l+r)>>1;  
  10.     //左右递归   
  11.     Build(l,m,rt<<1);  
  12.     Build(m+1,r,rt<<1|1);  
  13.     //更新信息   
  14.     PushUp(rt);  
  15. }  

(2)点修改:

假设A[L]+=C:
[cpp] view plain copy
 
 print?
  1. void Update(int L,int C,int l,int r,int rt){//l,r表示当前节点区间,rt表示当前节点编号  
  2.     if(l==r){//到叶节点,修改   
  3.         Sum[rt]+=C;  
  4.         return;  
  5.     }  
  6.     int m=(l+r)>>1;  
  7.     //根据条件判断往左子树调用还是往右   
  8.     if(L <= m) Update(L,C,l,m,rt<<1);  
  9.     else       Update(L,C,m+1,r,rt<<1|1);  
  10.     PushUp(rt);//子节点更新了,所以本节点也需要更新信息   
  11. }   

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


(3)区间查询(本题为求和):

询问A[L..R]的和
注意到,整个函数的递归过程中,L,R是不变的。
首先如果当前区间[l,r]在[L,R]内部,就直接累加答案
如果左子区间与[L,R]有重叠,就递归左子树,右子树同理。
[cpp] view plain copy
 
 print?
  1. int Query(int L,int R,int l,int r,int rt){//L,R表示操作区间,l,r表示当前节点区间,rt表示当前节点编号  
  2.     if(L <= l && r <= R){  
  3.         //在区间内,直接返回   
  4.         return Sum[rt];  
  5.     }  
  6.     int m=(l+r)>>1;  
  7.     //左子区间:[l,m] 右子区间:[m+1,r]  求和区间:[L,R]
  8.     //累计答案  
  9.     int ANS=0;  
  10.     if(L <= m) ANS+=Query(L,R,l,m,rt<<1);//左子区间与[L,R]有重叠,递归
  11.     if(R >  m) ANS+=Query(L,R,m+1,r,rt<<1|1); //右子区间与[L,R]有重叠,递归
  12.     return ANS;  
  13. }   




下面的是我自己的代码



#include<iostream>

#include<cstdio>

#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<cmath>
#include<map>
#include<set>
#include<vector>
#include<queue>
#define MAX 10000
using namespace std;
int n;
int c[MAX],b[MAX],a[MAX];
void build(int l,int r,int k) //建立一个线段树数组用来存放a数组的元素,便于查询和修改。其中l为左端点的下标,r为右端点的下标, k是线段树数组的下标
{
    if(l==r)    //如果左边和右边的相等,就把a数组的数存放到线段树数组中
    {
        b[k]=a[l];
        c[k]=a[l];
        //cout<<"b["<<k<<"] = "<<b[k]<<endl;
        return ;
    }
    int mid=(l+r)/2;
    build(l,mid,k*2);
    build(mid+1,r,k*2+1);
    b[k]=max(b[k*2],b[k*2+1]);   //b数组用来寻找最大值
    c[k]=c[k*2]+c[k*2+1];        //c数组用来区间求和
}
int sum(int l,int r,int k,int L,int R)  //给定一个区间,求这个区间内的和
{
    if(L>=l&&R<=r)
    {
        //cout<<k<<endl;
        return c[k];
    }
    int mid=(L+R)/2;
    int ans=0;
    if(l <= mid)
        ans+=sum(l , r , k*2 , L , mid);
    if(r > mid)
        ans+=sum(l , r , k*2+1 , mid+1 , R);
    return ans;
}


void update(int l,int r,int k,int L,int R,int x)  //给定一个区间,求这个区间内的和。L,R是线段树的总区间,l,r是需要修改的区间
{
    if(L==R)//当左端等于右端的时候,修改这个值
    {
        //cout<<k<<endl;
        c[k]=x;
        return ;
    }
    int mid=(L+R)/2;


    if(l <= mid)
        update(l , r , k*2 , L , mid , x);
    if(r > mid)
        update(l , r , k*2+1 , mid+1 , R , x);


    c[k]=c[k*2]+c[k*2+1];
}




int main()
{
    while(cin>>n)
    {
        for(int i=1;i<=n;i++)
        {
            cin>>a[i]; //原始数组
            //cout<<"a["<<i<<"] = "<<a[i]<<endl;
        }
        //cout<<endl;
        build(1,n,1); //建立线段树数组
        int x,y,z;
        cin>>x>>y>>z;
        for(int j=x;j<=y;j++)
        update(x,y,1,1,n,z);
        cout<<sum(x,y,1,1,n)<<endl;
    }
    return 0;
}
0 0
原创粉丝点击