链接来源: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;
}