前言
这篇文章,应该是我写的到目前为止最难也应该是最长的,(也不能说最难吧,反正也不知道怎么说)的一篇文章。
其实线段树吧,你之前可能听说过,是一种很难的算法。但是学过之后,又觉得不是那么难(反正我是这样的)。
为了保证你的阅读体验,请先学习:
- 树(数据结构)(必修)
- 位运算(必修)
- 树状数组(选修)
好的那么废话不多说我们开始吧。
为什么要有线段树
假设有一个数列{1,1,2,3,4,6}
需要实现以下两点:
- 对一个区间进行求和操作
- 对一个区间进行增加操作
我们逐个来看。
区间求和
首先第一点很容易实现:
for(int i = 1; i <= n; i++){
cin >> a[i];
}
for(int i = l; i <= r; i++){
sum+=a[i];
}
cout << sum;
这是 O ( n ) O(n) O(n)的时间复杂度。
如果想要 O ( 1 ) O(1) O(1)的时间复杂度怎么办呢?
那就用前缀和:
for(int i = 1; i <= n; i++){
cin >> a[i];
f[i]=f[i-1]+a[i];
}
cout << f[l]-f[r-1];
这样就实现了 O ( 1 ) O(1) O(1)的时间复杂度。
区间增加
这个也很简单。
for(int i = 1; i <= n; i++){
cin >> a[i];
}
for(int i = l; i <= r; i++){
a[i]+=t;
}
如果想要做到 O ( 1 ) O(1) O(1)的时间复杂度,那就要用差分数组。
for(int i = 1; i <= n; i++){
cin >> a[i];
c[i]=a[i]-a[i-1];
}
c[l]+=t;
c[r+1]-=t;
一个问题
假如两个一起来呢?
那怎么办?还能用前缀和与差分吗?
但是前缀和和差分不能一起用。。。。。
如果暴力的话,时间复杂度是 O ( t n ) ( t 是操作次数 n 是数据量 ) O(tn)(t\texttt{\small是操作次数}n\texttt{\small是数据量}) O(tn)(t是操作次数n是数据量)
这个时候,线段树就出现了。
线段树是什么
顾名思义,就是把一个树存在线段里面
而这棵树的叶子节点就是数组里面的数据,两个节点的爸爸节点就是它们的和。
让我们来看一张生动形象但并不怎么好看的图片:
有点丑,不要在意。
先看白色部分:
可以看到最底层只有一个数字,代表原数组的下标,而其他的(l~r)代表的是下标l到下标r的和。
再看黑色部分:
可以看到,黑色的数是从左到右,从上到下按顺序标的,而且还有一个规律:一个数所在的节点的左儿子的数是它的两倍,右儿子是它的两倍加一,这就是树状数组的下标。
好的,我们引出了一个概念,树状数组。
就是把一个树存进一个数组里面,对于每一个下标 i i i,他的左儿子是下标 2 i 2i 2i,右儿子是下标 ( 2 i + 1 ) (2i+1) (2i+1)。
那么现在又出来一个问题:
树状数组开多大
不想证明了,想要看证明的去这里,或者读以下片段(摘自前面的文章)
当 2 k ⩽ n ⩽ 2 k + 1 \large 2^{k}\leqslant n\leqslant 2^{k+1} 2k⩽n⩽2k+1,即 k ⩽ log 2 n ⩽ k + 1 \large k\leqslant \log_{2}n\leqslant k+1 k⩽log2n⩽k+1时,线段数所开大小与叶子数为 2 ( k + 1 ) 2^(k+1) 2(k+1)的满二叉树大小相同,而该满二叉树的节点数 s i z e = 2 ( k + 2 ) − 1 size=2^{(k+2)}-1 size=2(k+2)−1。
由 2 k ⩽ n ⩽ 2 k + 1 \large 2^{k}\leqslant n\leqslant 2^{k+1} 2k⩽n⩽2k+1可知 ⌈ log 2 n ⌉ = k + 1 \large \left \lceil \log_{2}n \right \rceil=k+1 ⌈log2n⌉=k+1,则
2 k + 2 − 1 = 2 ⌈ log 2 n ⌉ + 1 − 1 = 2 ⌈ log 2 ( n ∗ 2 ) ⌉ − 1 \large 2^{k+2}-1=2^{\left \lceil \log_{2}n \right \rceil+1}-1=2^{\left \lceil \log_{2}(n*2) \right \rceil}-1 2k+2−1=2⌈log2n⌉+1−1=2⌈log2(n∗2)⌉−1
2 k + 2 = 2 ⌈ l o g 2 ( n ∗ 2 ) ⌉ \large 2^{k+2}=2^{\left \lceil log_{2}(n*2) \right \rceil} 2k+2=2⌈log2(n∗2)⌉
因为 log 2 ( n ∗ 2 ) ⩽ ⌈ log 2 ( n ∗ 2 ) ⌉ < l o g 2 ( n ∗ 2 ) + 1 = l o g 2 ( n ∗ 4 ) \large \log_{2}(n*2)\leqslant \left \lceil \log_{2}(n*2) \right \rceil< log_{2}(n*2)+1=log_{2}(n*4) log2(n∗2)⩽⌈log2(n∗2)⌉<log2(n∗2)+1=log2(n∗4)
所以 2 log 2 ( n ∗ 2 ) ⩽ 2 ⌈ log 2 ( n ∗ 2 ) ⌉ < 2 l o g 2 ( n ∗ 2 ) + 1 = 2 l o g 2 ( n ∗ 4 ) \large 2^{\log_{2}(n*2)}\leqslant 2^{\left \lceil \log_{2}(n*2) \right \rceil}< 2^{log_{2}(n*2)+1}=2^{log_{2}(n*4)} 2log2(n∗2)⩽2⌈log2(n∗2)⌉<2log2(n∗2)+1=2log2(n∗4)
变化一下: 2 n ⩽ 2 k + 2 < 4 n \large 2n\leqslant 2^{k+2}< 4n 2n⩽2k+2<4n
最终 2 n − 1 ⩽ 2 k + 2 − 1 < 4 n − 1 \large 2n-1\leqslant 2^{k+2}-1< 4n-1 2n−1⩽2k+2−1<4n−1,即 2 n − 1 ⩽ s i z e < 4 n − 1 \large 2n-1\leqslant size<4n-1 2n−1⩽size<4n−1
如有侵权,会删除(打 LaTeX \LaTeX LATEX用了好久)
结论:开 4 n 4n 4n即可。
现在开始正文。
建树
首先定义以下变量:
const int N=1005;
int n;
int tree[N>>2];
然后利用递归的方式建树。
void push_up(int node){//用于更新一个节点
tree[node]=tree[node>>1]+tree[(node>>1)+1]
}
void build(int node,int l,int r){
if(l==r){
cin >> tree[node];//已经到叶子节点了,注意看上面那张图,叶子节点的l和r是相等的(所以我只写了一个数)。
return;//结束
}
int mid = l+r>>1;
build(node*2,l,mid);//左子树递归
build(node*2+1,mid+1,r);//右子树递归
push_up(node);//回溯更新父节点
}
然后主函数的话就比较简单了。。。
int main()
{
cin >> n;
build(1,1,n);
}
单点修改
单点修改很简单,类似与二分查找。
//node是当前树节点,lr是查找范围,id是数组下标,k是要增加的数
void change(int node,int l,int r,int id,int k)
{
if(l==r){
tree[node]+=k;//更新叶子节点
}
int mid=l+r>>1;
if(id<=mid){//如果这个节点小于mid
change(node>>1,l,mid,id,k);//找左子树
}
else//否则找右子树
{
change((node>>1)+1,mid+1,r,id,k);
}
push_up(node);//回溯更新父节点
}
懒标记
设想一个问题,假如让你对一个数组操作10亿次,再查询一次,那时间消耗将是巨大的,所以我们不妨先将操作记录下来,到要使用的时候,再去操作,这就是懒标记。
当使用到某个节点的时候,如果发现懒标记存在,那么就应该更新该节点,以及以下的所有节点的数据,方便使用。
我们开一个懒标记的数组(下文简称懒数组)。
int lazy[N>>2];
懒数组是用来记录单个节点要增加多少的。
我们需要定义一个函数来传递懒标记。
void push_down(int node,int l,int r){
if(lazy[node]){
int mid = l+r>>1;
lazy[node>>1] += lazy[node];
lazy[(node>>1)+1] += lazy[node];
//顺便更新子节点
tree[node>>1] += (mid-(l-1))*lazy[node];
tree[(node>>1)+1] += (r-mid)*lazy[node];
lazy[node] = 0;//清空该节点的懒标记
}
}