线段树详解

1.什么是线段树

线段树是一颗完全二叉树,树上的每个节点都维护一个区间。根维护的是整个区间,每个节点维护的是父亲的区间二等分后的其中一个子区间。当有n个元素时,对区间的操作可以在O(logn)的时间内完成。
根据节点中维护的数据不同的不同,线段树可以提供不同的功能,如RMQ、RSQ等。
对于数组[2, 5, 1, 4, 9, 3]可以构造如下的二叉树(背景为白色表示叶子节点,非叶子节点的值是其对应数组区间内的最小值)
对于数组[2, 5, 1, 4, 9, 3]可以构造如下的二叉树

2.线段树的结构

对于线段树我们可以选择和普通二叉树一样的链式结构。由于线段树是完全二叉树,我们也可以用数组来存储,下面的讨论及代码都是数组来存储线段树(注意到用数组存储时,有效空间为2n-1,实际空间确不止这么多,比如上面的线段树中叶子节点1、3虽然没有左右子树,但是的确占用了数组空间,实际空间是满二叉树的节点数目,一般线段树数组长度是叶子结点个数四倍 就够了(想想为什么)。
线段树基本要实现如下功能,代码在实现中给出详细注释

  • 建树与维护
    定义包含n个节点的线段树数组dat[n],dat[0]表示根节点。那么对于节点dat[i],它的左孩子是dat[2i+1],右孩子是dat[2i+2]。
  • 区间修改(包括单点修改)
    给定一个区间(或一个下标),把区间内每个数都加上(乘以)一个数。
  • 区间查询
    给定一个区间,求区间内所有数的最大/小值(RMQ)或每一个数的和(RSQ)。

再说一下线段树的精华——懒标记(lazyTag)
懒标记:每个节点新增加一个标记,记录这个节点是否进行了某种修改(这种修改操作会影响其子节点),对于任意区间的修改,我们先按照区间查询的方式将其划分成线段树中的节点,然后修改这些节点的信息,并给这些节点标记上代表这种修改操作的标记。在修改和查询的时候,如果我们到了一个节点p,并且决定考虑其子节点,那么我们就要看节点p是否被标记,如果有,就要按照标记修改其子节点的信息,并且给子节点都标上相同的标记,同时消掉节点p的标记。懒标记确保了线段树操作区间的时间复杂度在O(logn)。

3.线段树的具体实现(注释非常详细)

还是通过一道例题来形象地解释吧 线段树模板
c++代码

#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn=1e5+7;
ll n,m,a[maxn],dat[4*maxn],tag[4*maxn];  //区间长度、询问个数、存储叶子结点数组、存储线段树数组 、懒标记数组

/*
功能:自下往上更新父亲节点
root:当前根节点在线段树数组中的下标
*/ 
void pushUp(int root){
	dat[root]=dat[root*2+1]+dat[root*2+2]; //求区间和
	//dat[root]=min(dat[root*2+1],dat[root*2+2]);  求区间最小值 
	//dat[root]=max(dat[root*2+1],dat[root*2+2]);  求区间最大值 
}

/*
功能:构建线段树
root:当前线段树的根节点下标
l:数组的起始位置
r:数组的结束位置
*/
void build(int root,int l,int r){
	tag[root]=0;              //设置懒标记 
	if(l==r) dat[root]=a[l]; //如果是叶子节点
	else{
		int mid=l+r>>1;
		build(2*root+1,l,mid);   //递归构造左子树 
		build(2*root+2,mid+1,r); //递归构造右子树 
		pushUp(root);  //根据左右子树根节点的值,更新当前根节点的值
	}
}

/*
功能:自上往下传递懒标记 
root: 当前线段树的根节点下标
l:数组的起始位置
r:数组的结束位置
*/
void pushDown(int root,int l,int r){
	if(tag[root]){
		//设置左右孩子节点的标志域
		tag[root*2+1]+=tag[root];
		tag[root*2+2]+=tag[root];
		//根据标志域设置孩子节点的值。由于是这个区间统一改变,所以dat数组要加元素个数次
		int mid=l+r>>1;
		dat[root*2+1]+=(mid-l+1)*tag[root]; //求最大/小值时加1即可 
		dat[root*2+2]+=(r-mid)*tag[root];
		//传递后,当前节点懒标记清空
		tag[root]=0;
	} 
}

/*
功能:更新线段树中某个区间内叶子节点的值
root:当前线段树的根节点下标
[l,r]: 当前节点所表示的区间
[ul,ur]: 待更新的区间
k: 更新的值(原来的值加上k)
*/
void update(int root,int l,int r,int ul,int ur,int k){
	if(ur<l||ul>r) return; //更新区间和当前节点区间没有交集
	else if(ul<=l&&ur>=r){ //当前节点区间包含在更新区间内
		tag[root]+=k;
		dat[root]+=(r-l+1)*k;
		return;
	}
	pushDown(root,l,r);  //延迟标记向下传递
	 //更新左右孩子节点
	int mid=l+r>>1;   
	if(ul<=mid) update(root*2+1,l,mid,ul,ur,k);
	if(ur>mid) update(root*2+2,mid+1,r,ul,ur,k);
	pushUp(root);  //根据左右子树的值回溯更新当前节点的值
} 

/*
功能:线段树的区间查询
root:当前线段树的根节点下标
[l, r]: 当前节点所表示的区间
[ql, qr]: 此次查询的区间
*/
ll query(int root,int l,int r,int ql,int qr){
	ll ans=0;  //记录区间和 
	if(ql<=l&&qr>=r) return dat[root]; //当前节点区间包含在查询区间内
	pushDown(root,l,r); //懒标记向下传递
	 //分别从左右子树查询,将查询到的值加入结果中
	int mid=l+r>>1;
	if(ql<=mid) ans+=query(root*2+1,l,mid,ql,qr);
	if(qr>mid) ans+=query(root*2+2,mid+1,r,ql,qr);
	return ans; //返回查询结果 
}
int main(){
	cin>>n>>m;
	for(int i=0;i<n;i++)
	cin>>a[i];
	build(0,0,n-1);  //题目数组下标是从1开始的,所以这里我们要减1,下同
	for(int i=0;i<m;i++){
		int p,x,y,k;
		cin>>p;
		if(p==1){
			cin>>x>>y>>k;
			update(0,0,n-1,x-1,y-1,k); //注意区间两个端点也都要减1
		}
		else{
			cin>>x>>y;
			cout<<query(0,0,n-1,x-1,y-1)<<endl;
		}
	} 
	return 0;
} 

4.线段树的应用

线段树2.0
这道题是上面那道模板题的进化版,可以很好地检验你对线段树的掌握程度
具体思路:这道题其实就是考验我们对线段树的懒标记理解程度。在尝试着写了只有一个lazetag的程序之后我们发现一个lazytag是不能够解决问题的,那就上两个,分别表示乘法意义上的lazytag和加法意义上的lazytag。另外由于乘法的优先级比加法高,我们要先乘后加,并且在更新乘法懒标记时也要更新一下加法懒标记(我也思考了很久)。
参考代码如下(就只是把上面代码略微改动了一下,另外要注意的是要取模)

#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn=1e5+7;
ll n,m,mod,a[maxn],dat[4*maxn],tag[4*maxn],mul[4*maxn];  //区间长度、询问个数、存储叶子结点数组、存储线段树数组 、加法懒标记数组、乘法懒标记数组

/*
功能:自下往上更新父亲节点
root:当前根节点在线段树数组中的下标
*/ 
void pushUp(int root){
	dat[root]=(dat[root*2+1]+dat[root*2+2])%mod; //求区间和
	//dat[root]=min(dat[root*2+1],dat[root*2+2]);  求区间最小值 
	//dat[root]=max(dat[root*2+1],dat[root*2+2]);  求区间最大值 
}

/*
功能:构建线段树
root:当前线段树的根节点下标
l:数组的起始位置
r:数组的结束位置
*/
void build(int root,int l,int r){
	tag[root]=0;              //设置懒标记 
	mul[root]=1;
	if(l==r) dat[root]=a[l]; //如果是叶子节点
	else{
		int mid=l+r>>1;
		build(2*root+1,l,mid);   //递归构造左子树 
		build(2*root+2,mid+1,r); //递归构造右子树 
		pushUp(root);  //根据左右子树根节点的值,更新当前根节点的值
	}
}

/*
功能:自上往下传递懒标记 
root: 当前线段树的根节点下标
l:数组的起始位置
r:数组的结束位置
*/
void pushDown(int root,int l,int r){
		//设置左右孩子节点的标志域
		tag[root*2+1]=(tag[root*2+1]*mul[root]+tag[root])%mod;
		tag[root*2+2]=(tag[root]+mul[root]*tag[root*2+2])%mod;
		mul[root*2+1]=(mul[root*2+1]*mul[root])%mod;
		mul[root*2+2]=(mul[root*2+2]*mul[root])%mod;
		//根据标志域设置孩子节点的值。由于是这个区间统一改变,所以dat数组要加元素个数次
		int mid=l+r>>1;
		dat[root*2+1]=((mid-l+1)*tag[root]+dat[root*2+1]*mul[root])%mod; //求最大/小值时加1即可 
		dat[root*2+2]=((r-mid)*tag[root]+dat[root*2+2]*mul[root])%mod;
		//传递后,当前节点懒标记清空
		tag[root]=0;
		mul[root]=1; 
}

/*
功能:更新线段树中某个区间内叶子节点的值
root:当前线段树的根节点下标
[l,r]: 当前节点所表示的区间
[ul,ur]: 待更新的区间
k: 更新的值(原来的值加上k)
*/
void update1(int root,int l,int r,int ul,int ur,int k){ //加法
	if(ul>r||ur<l) return; //更新区间和当前节点区间没有交集
	else if(ul<=l&&ur>=r){ //当前节点区间包含在更新区间内
		tag[root]=(tag[root]+k)%mod;
		dat[root]=(dat[root]+(r-l+1)*k)%mod;
		return;
	}
	pushDown(root,l,r);  //延迟标记向下传递
	 //更新左右孩子节点
	int mid=l+r>>1;   
	if(ul<=mid) update1(root*2+1,l,mid,ul,ur,k);
	if(ur>mid) update1(root*2+2,mid+1,r,ul,ur,k);
	pushUp(root);  //根据左右子树的值回溯更新当前节点的值
} 
void update2(int root,int l,int r,int ul,int ur,int k){  //乘法
	if(ul>r||ur<l) return; //更新区间和当前节点区间没有交集
	else if(ul<=l&&ur>=r){ //当前节点区间包含在更新区间内
		tag[root]=(tag[root]*k)%mod;
		mul[root]=(mul[root]*k)%mod;
		dat[root]=(dat[root]*k)%mod;
		return;
	}
	pushDown(root,l,r);  //延迟标记向下传递
	 //更新左右孩子节点
	int mid=l+r>>1;   
	if(ul<=mid) update2(root*2+1,l,mid,ul,ur,k);
	if(ur>mid) update2(root*2+2,mid+1,r,ul,ur,k);
	pushUp(root);  //根据左右子树的值回溯更新当前节点的值
} 

/*
功能:线段树的区间查询
root:当前线段树的根节点下标
[l, r]: 当前节点所表示的区间
[ql, qr]: 此次查询的区间
*/
ll query(int root,int l,int r,int ql,int qr){
	ll ans=0;  //记录区间和 
	if(ql<=l&&qr>=r) return dat[root]; //当前节点区间包含在查询区间内
	pushDown(root,l,r); //懒标记向下传递
	 //分别从左右子树查询,将查询到的值加入结果中
	int mid=l+r>>1;
	if(ql<=mid) ans=(ans+query(root*2+1,l,mid,ql,qr))%mod;
	if(qr>mid) ans=(ans+query(root*2+2,mid+1,r,ql,qr))%mod;
	return ans%mod; //返回查询结果 
}
int main(){
	cin>>n>>m>>mod;
	for(int i=0;i<n;i++)
	cin>>a[i];
	build(0,0,n-1);
	for(int i=0;i<m;i++){
		int p,x,y,k;
		cin>>p;
		if(p==1){
			cin>>x>>y>>k;
			update2(0,0,n-1,x-1,y-1,k);
		}
		else if(p==2){
			cin>>x>>y>>k;
			update1(0,0,n-1,x-1,y-1,k);
		}
		else{
			cin>>x>>y;
			cout<<query(0,0,n-1,x-1,y-1)<<endl;
		}
	} 
	return 0;
} 

后面有经典题会不断更新

如果想要得到更多知识,请关注我博客:wlis.blog.csdn.net

此博客不定期更新内容!!!感谢大家!!!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值