今天我们来学习线段树这一数据结构。线段树是一种二叉树,它的每一个节点存储一段区间的信息(如该区间内所有元素的和)。以终点为分界,每个节点衍生出两个子节点,直到某个节点表示的区间只有一个数。如图——
线段树(第一版)
我们可以利用线段树来实现题目的需求。现在我们假设数列中有五个数,那么线段树如下——
第一种操作
线段树中的每个端点存储的是这个端点所代表的区间内的元素的和。假若我们现在要给第三个数字加上y,那么我们可以发现,从第三个数字所在的最小区间(即只有它自己的区间)到根节点所表示的区间路径上的点表示的区间都要加y,这是因为它们都包含第三个数。
第二种操作
要求得左端点为x右端点为y的区间内的元素和,我们的基本想法是把这个大区间分成一个个小区间,分别对每个小区间求和再加起来。在这个过程中,如果我们把某个节点的两个子节点都选上了,那么就可以直接选该节点而不选子节点,从而减少计算量。譬如我们要求【3,5】这个区间的元素和,我们把它分成【3,3】,【4,4】和【5,5】三个小区间。由于【4,5】的两个子区间都选了,我们就可以直接选【4,5】来代替它们两个。这样一来,由于区间是连续的,每层最多只可能选两个靠边的区间。如图——
要求绿颜色范围内的区间,最后一层最多只会选两个区间。下面让我们来看看两种操作具体的代码实现吧!
#include<bits/stdc++.h>
using namespace std;
//开数列元素的四倍大小来存储节点,这里不加证明
const int N = 2000010,M = 500010;
//f[i]表示第i个节点表示的区间(节点与区间时一一对应的)的元素和,root表示读入的原数列
int n,m,f[N],root[N];
inline void buildtree(int k,int l,int r)
{
if(l==r)
{
//如果l==r,说明递归到叶子节点(即一个区间只有一个值),那么此时的f[k](即下标为k的区间内元素的和)就是root[l]本身
f[k] = root[l];
//这里不要忘记return!
return;
}
//取出区间的中点,递归地建立左子树和右子树
int m = (l+r)>>1;
buildtree(k+k,l,m);
buildtree(k+k+1,m+1,r);
//别忘了这一句话,要把左右两个子节点代表的区间的元素和加起来赋给父节点
f[k] = f[k+k]+f[k+k+1];
}
//这句话意思是在下标k表示的左端点为l右端点为r的区间中的编号为x的数加上y
inline void add(int k,int l,int r,int x,int y)
{
//编号为k的区间内的点x加上了y,那么该区间和自然也加上y,因为该区间含有x
f[k]+=y;
//如果l==r,说明已经递归到叶子节点,说明该路径上含有x的区间的和都加上了y,返回即可
if(l==r) return;
int m = (l+r)>>1;
//如果x落在左子树内,就递归地让左子树上含有x的区间和都加上y。x在右子树类似
if(x<=m) add(k+k,l,m,x,y);
else add(k+k+1,m+1,r,x,y);
}
//这句话的意思是,在节点k代表的区间【l,r】内求区间【s,t】的元素和
inline int calculate(int k,int l,int r,int s,int t)
{
//如果l==s&&r==t,那么要查询的区间就是当前区间,直接返回节点k代表的区间的元素和即可
if(l==s&&r==t) return f[k];
int m = (l+r)>>1;
//t<=m说明要查询的区间完全落在左半边
if(t<=m) return calculate(k+k,l,m,s,t);
//s>m说明要查询的区间完全落在右半边
else if(s>m) return calculate(k+k+1,m+1,r,s,t);
//否则要查询的区间横跨左右两边,只要分别查询并求和即可,相当于剖成两个区间了
else return calculate(k+k,l,m