本篇只对线段树的基本应用介绍:即整区间单次改变,区间求和,单点改变和区间改变一个道理,只把区间变成点。
剩下的线段树知识点类似:区间求逆序对,区间多次改变(同时+,再*或者/或者-),区间(合并,交),区间过大在改变时对p取模等放在后面学习给出。
若只是需要求区间和或者单点改变,树状数组是个好的选择,但是其他的就老老实实线段树了。线段树由于本身是专门用来处理区间问题的(包括RMQ、RSQ问题等)。
线段树只是一个树的结构(完全二叉树),和树本身没有关系。
线段树的空间要开4倍最大的空间,用来存树的结构。
(图片来源互联网)
对于每一个子节点而言,都表示整个序列中的一段子区间;对于每个叶子节点而言,都表示序列中的单个元素信息;子节点不断向自己的父亲节点传递信息,而父节点存储的信息则是他的每一个子节点信息的整合。说到底就是运用分块的思想,用于达到O(logn)级别的处理速度,log以2为底。
观察上图可以得出,左孩子编号是i*2,右孩子编号是i*2+1,(完全二叉树);
二进制位左移一位代表着数值∗2,而如果左移完之后再或上1,由于左移完之后最后一位二进制位上一定会是0,所以∣1等价于+1
#define lson l,m,rt<<1 //lson表示rt节点的左孩子
#define rson m+1,r,rt<<1|1 //rson表示右孩子 后面会多次用到,可以先预处理
先介绍pushup函数,根据二叉树的特性,从下到上维护,pushup操作的目的是为了维护父子节点之间的逻辑关系。当我们递归建树时,对于每一个节点我们都需要遍历一遍,并且电脑中的递归实际意义是先向底层递归,然后从底层向上回溯,所以开始递归之后必然是先去整合子节点的信息,再向它们的祖先回溯整合之后的信息。
void pushup(ll rt){
sum[rt] = sum[rt<<1] +sum[rt<<1|1];
maxs[rt] = max (maxs[rt<<1],maxs[rt<<1|1]);//最大值同理
}
由此得到建树,由于二叉树自身的父子节点之间的可传递关系,所以可以考虑递归建树(,并且在建树的同时,我们应该维护父子节点的关系。
void build(ll l,ll r,ll rt){
lazy[rt]=0;//这和后面的懒惰标记有关,可以想往后看
if(l==r){
sum[rt] = a[l];
return;
}
ll m=(l+r)>>1;
build(lson);
build(rson);
pushup(rt); //此处由于我们是要通过子节点来维护父亲节点,所以pushup的位置应当是在回溯时。
}
再说下成段更新,把一个区间的数全+k,这里引出线段树第一个难点,需要用到延迟标记(或者说懒惰标记),简单来说就是每次更新的时候不要更新到底,用延迟标记使得更新延迟到下次需要更新 or 询问到的时候。
我们就需要在每次区间的查询修改时pushdown一次,以免重复或者冲突或者爆炸。
那么对于pushdown而言,其实就是纯粹的pushup的逆向思维(但不是逆向操作): 因为修改信息存在父节点上,所以要由父节点向下传导lazy tag。
void pushdown(ll rt,ll z){
if(lazy[rt]){ //如果这个点有懒惰标志,说明应该改变
lazy[rt<<1] += lazy[rt];
lazy[rt<<1|1] += lazy[rt];
sum[rt<<1] += (z-(z>>1))* lazy[rt];//由于是这个区间统一改变,所以sum数组要加元素个数次
sum[rt<<1|1] += (z>>1) * lazy[rt];//右儿子个数*加上的数。
lazy[rt]=0;
}
}
void updata(ll nl,ll nr,ll k,ll l,ll r,ll rt){//nl,nr为要修改的区间,k是区间要+的值
//l,r,rt为当前节点所存储的区间以及节点的编号
if(nl<=l && r<=nr){ //如果我要改变的区间是rt节点的父类节点,那么我放在下次用到的时候一起更新,现在只改变当前rt区间的值。
lazy[rt] += k;
sum[rt] += k*(r-l+1);//k*我这个区间的所有孩子个数
return;
}
pushdown(rt,r-l+1);
//回溯之前(也可以说是下一次递归之前,因为没有递归就没有回溯)
//由于是在回溯之前不断向下传递,所以自然每个节点都可以更新到
ll m = (l+r)>>1;
if(nl<=m) updata(nl,nr,k,lson);
if(nr>m) updata(nl,nr,k,rson);
pushup(rt);//回溯之后维护父节点
}
区间查找:和区间改变差不多思想,都是用分块的方式,不断递归
ll query(ll nl,ll nr,ll l,ll r,ll rt){
ll ans=0;
if(nl<=l && r<=nr){//从上到下找到了我需要的区间
return sum[rt];
}
pushdown(rt,r-l+1);//看看有没有之前懒得没改的
ll m = (l+r)>>1;
if(nl <= m) ans+=query(nl,nr,lson);
if(m < nr) ans+=query(nl,nr,rson);
return ans;
}
最后给出模板:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll maxn = 1e5+10;
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
ll a[maxn],lazy[maxn<<2],sum[maxn<<2],maxs[maxn<<2];
ll n,m;
void pushup(ll rt){
sum[rt] = sum[rt<<1] +sum[rt<<1|1];
maxs[rt] = max (maxs[rt<<1],maxs[rt<<1|1]);
}
void pushdown(ll rt,ll z){
if(lazy[rt]){
lazy[rt<<1] += lazy[rt];
lazy[rt<<1|1] += lazy[rt];
sum[rt<<1] += (z-(z>>1))* lazy[rt];
sum[rt<<1|1] += (z>>1) * lazy[rt];
lazy[rt]=0;
}
}
void build(ll l,ll r,ll rt){
lazy[rt]=0;
if(l==r){
sum[rt] = a[l];
return;
}
ll m=(l+r)>>1;
build(lson);
build(rson);
pushup(rt);
}
void updata(ll nl,ll nr,ll k,ll l,ll r,ll rt){
if(nl<=l && r<=nr){
lazy[rt] += k;
sum[rt] += k*(r-l+1);
return;
}
pushdown(rt,r-l+1);
ll m = (l+r)>>1;
if(nl<=m) updata(nl,nr,k,lson);
if(nr>m) updata(nl,nr,k,rson);
pushup(rt);
}
ll query(ll nl,ll nr,ll l,ll r,ll rt){
ll ans=0;
if(nl<=l && r<=nr){
return sum[rt];
}
pushdown(rt,r-l+1);
ll m = (l+r)>>1;
if(nl <= m) ans+=query(nl,nr,lson);
if(m < nr) ans+=query(nl,nr,rson);
return ans;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++)
scanf("%lld",&a[i]);
build(1,n,1);
for(int i=1;i<=m;i++){
ll w,a,b,k;
scanf("%lld",&w);
if(w==1){
scanf("%lld %lld %lld",&a,&b,&k);
updata(a,b,k,1,n,1);
}else if(w==2){
scanf("%lld %lld",&a,&b);
printf("%lld\n",query(a,b,1,n,1));
}
}
return 0;
}
样例测试:
5 5 //5个数5次操作
1 5 4 2 3
2 2 4 //2是求出a-b区间
1 2 3 2 //1是a-b区间都+k
2 3 4
1 1 5 1
2 1 4
输出:11
8
20