1.什么是线段树
线段树是一颗完全二叉树,树上的每个节点都维护一个区间。根维护的是整个区间,每个节点维护的是父亲的区间二等分后的其中一个子区间。当有n个元素时,对区间的操作可以在O(logn)的时间内完成。
根据节点中维护的数据不同的不同,线段树可以提供不同的功能,如RMQ、RSQ等。
对于数组[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;
}
后面有经典题会不断更新