线段树模板(1)

本篇只对线段树的基本应用介绍:即整区间单次改变,区间求和,单点改变和区间改变一个道理,只把区间变成点。

剩下的线段树知识点类似:区间求逆序对,区间多次改变(同时+,再*或者/或者-),区间(合并,交),区间过大在改变时对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

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值