线段树学习笔记
线段树简介
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为 \(O(logN)\) 。而未优化的空间复杂度为 \(2N\) ,实际应用时一般还要开4N的数组以免越界,因此有时需要离散化让空间压缩。
——百度百科
线段树是一种基于分治思想的二叉树结构,用于区间信息统计
线段树对比树状数组有如下好处:
- 每个节点都代表一个区间
- 具有唯一根节点,代表 \([1,N]\)
- 每个叶子节点代表一个元素
- 可以用二叉树的方式存储(详见下面)
因为线段树接近完全二叉树,故可以用如下表示方法:
- 根节点编号为 \(1\)
- 编号为 \(X\) 的节点左子节点编号为 \(2X\)
- 编号为 \(X\) 的节点右子节点编号为 \(2X+1\)
线段树特性:
- 长度是 \(n\) 的序列构造线段树,这颗线段树有 \(2n-1\) 个节点(同二叉树叶子节点与所有节点数量的关系),高度为 \(logn\)。
- 存线段树的数组要开 \(\mathbf{4}\) 倍空间!
线段树实现
1.建树
先定义一个线段树的结构体:
struct tree{
int l,r;//区间信息
int sum,tag;//区间和及标记
//其他变量的定义参考题目
}t[40010];
再从上到下构建线段树,并从下往上传值,可用递归实现
void build(int x,int l,int r){
t[x].l=l,t[x].r=r;//传递区间[l,r]
if(l==r){
t[x].sum=a[l];
return;
}//此点如是叶子节点则结束递归
int mid=(l+r)>>1;//区间中点
build(x*2,l,mid);//构造左子树
build(x*2+1,mid+1,r);//构造右子树
t[x].sum=t[x*2].sum+t[x*2+1].sum;//从下往上传值
}
调用入口:build(1,1,n);
2.基础修改与查询
1.单点修改与查询
线段树从根节点开始执行指令,我们可以通过递归找到要修改的节点,然后从下往上更新经过的所有节点(时间复杂度 \(O(logn)\) )
我们如果更改点 \(1\) ,需要更改的节点如图红圈部分:
void change(int x,int u,int a){
if(t[x].l==t[x].r){//找到目标更改
t[x].sum=a;
return;
}
int mid=(t[x].l+t[x].r)>>1;//区间中点
if(mid>=u)change(x*2,u,a);//u属于左半部分
else change(x*2+1,u,a);//u属于右半部分
t[x].sum=t[x*2].sum+t[x*2+1].sum;//刷新值
}
调用入口:change(1,x,a);
单点查询同理,只是不用回溯
int ask(int x,int u){
if(t[x].l==t[x].r)return t[x].sum;//找到目标返回值
int mid=(t[x].l+t[x].r)>>1;//区间中点
if(mid>=u)ask(x*2,u);//u属于左半部分
else ask(x*2+1,u);//u属于右半部分
}
调用入口:ask(1,x);
2.区间查询(基础)
区间查询其实并不难,只要递归执行以下步骤:
- 若 \([l,r]\) 完全覆盖了整个区间,立刻回溯
- 若左子节点与 \([l,r]\) 有重叠部分,递归访问左子节点
- 若右子节点与 \([l,r]\) 有重叠部分,递归访问右子节点
我们如果查询区间 \([2,7]\) ,需要查询的节点如图红圈部分:
int ask(int x,int l,int r){
if(l<=t[x].l&&r>=t[x].r)return t[x].sum;//完全包含
int mid=(t[x].l+t[x].r)>>1;//区间中点
int sum=0;
if(mid>=l)sum+=ask(x*2,l,r);//访问左半部分
if(mid<r)sum+=ask(x*2+1,l,r);//访问右半部分
return sum;
}
调用入口:ask(1,l,r);
学了这么多,练习一下吧!
例题1
luogu P3374 【模板】树状数组 1
别看这道题题目是树状数组,其实用线段树单点修改,区间查询也是可以的
点击查看题目
#include<bits/stdc++.h>
using namespace std;
int n,m,a[4000010];
struct tree{
int l,r;//区间信息
int sum,tag;//区间和及标记
//其他变量的定义参考题目
}t[4000010];
void build(int x,int l,int r){
t[x].l=l,t[x].r=r;//传递区间[l,r]
if(l==r){
t[x].sum=a[l];
return;
}//此点如是叶子节点则结束递归
int mid=(l+r)>>1;//区间中点
build(x*2,l,mid);//构造左子树
build(x*2+1,mid+1,r);//构造右子树
t[x].sum=t[x*2].sum+t[x*2+1].sum;//从下往上传值
}
void change(int x,int u,int a){
if(t[x].l==t[x].r){//找到目标增加
t[x].sum+=a;
return;
}
int mid=(t[x].l+t[x].r)>>1;//区间中点
if(mid>=u)change(x*2,u,a);//u属于左半部分
else change(x*2+1,u,a);//u属于右半部分
t[x].sum=t[x*2].sum+t[x*2+1].sum;//刷新值
}
int ask(int x,int l,int r){
if(l<=t[x].l&&r>=t[x].r)return t[x].sum;//完全包含
int mid=(t[x].l+t[x].r)>>1;//区间中点
int sum=0;
if(mid>=l)sum+=ask(x*2,l,r);//访问左半部分
if(mid<r)sum+=ask(x*2+1,l,r);//访问右半部分
return sum;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>a[i];
build(1,1,n);//建树
for(int i=1;i<=m;i++){
int f,x,y;
cin>>f>>x>>y;
if(f==1){
change(1,x,y);//单点修改
}else{
cout<<ask(1,x,y)<<endl;//区间查询
}
}
return 0;
}
延时标记
1.简介
一般区间修改是这样的:
for(int i=l;i<=r;i++){
change(1,i,a);
}
如果我们更改区间 \([3,8]\) ,要更改的点如下图,其中红圈表示循环中直接更改的点,绿圈表示递归中要更改的点
是不是要更改很多点,这样做时间复杂度都退化到 \(O(n logn)\) 了,比模拟法都慢
我们可以先不全修改,打上标记以后修改,但用的时候怎么办呢?我们可以到用的时候再更新,这样就可以把时间复杂度降到 \(O(logn)\)了
如下图,我们只要修改红圈部分,并在完全覆盖的地方(绿圈部分)打上标记
2.实现
1.建树
建树函数没有变化
void build(int x,int l,int r){
t[x].l=l,t[x].r=r;//传递区间[l,r]
if(l==r){
t[x].sum=a[l];
return;
}//此点如是叶子节点则结束递归
int mid=(l+r)>>1;//区间中点
build(x*2,l,mid);//构造左子树
build(x*2+1,mid+1,r);//构造右子树
t[x].sum=t[x*2].sum+t[x*2+1].sum;//从下往上传值
}
调用入口:build(1,1,n);
2.下传标记
下传标记其实很简单,都不用递归,只有更改下面左右子树的值和标记,再刷新自己的值就行了
void down(int x){
if(t[x].tag){//如果有标记
t[x*2].tag+=t[x].tag;//下传左子树
t[x*2+1].tag+=t[x].tag;//下传右子树
t[x*2].sum+=(t[x*2].r-t[x*2].l+1)*t[x].tag;//左子树和增加
t[x*2+1].sum+=(t[x*2+1].r-t[x*2+1].l+1)*t[x].tag;//右子树和增加
t[x].tag=0;//清空标记
}
}
调用入口:down(x);
3.更改
更改也没很大差别,区别是到所有点如果完全包含就标记,叶子节点直接返回(因为没有要下传子节点了),两者都不成立就下传标记并继续递归
增加代码:
if(t[x].l>=l&&t[x].r<=r){//完全包含
t[x].tag+=a;//标记区间
t[x].sum+=(t[x].r-t[x].l+1)*a;//区间和增加
return;
}
if(t[x].l==t[x].r)return;//到叶子节点直接返回
down(x);//这时还没有操作就需下传标记
全代码:
void change(int x,int l,int r,int a){
if(t[x].l>=l&&t[x].r<=r){//完全包含
t[x].tag+=a;//标记区间
t[x].sum+=(t[x].r-t[x].l+1)*a;//区间 和增加
return;
}
if(t[x].l==t[x].r)return;//到叶子节点直接返回
down(x);//这时还没有操作就需下传标记
int mid=(t[x].l+t[x].r)>>1;//区间中点
if(mid>=l)change(x*2,l,r,a);//访问左半部分
if(mid<r)change(x*2+1,l,r,a);//访问右半部分
t[x].sum=t[x*2].sum+t[x*2+1].sum;//刷新值
}
调用入口:build(1,1,n);
4.查询
查询基本没变化,只要在不完全包含时下传标记再递归就行了
int ask(int x,int l,int r){
if(l<=t[x].l&&r>=t[x].r)return t[x].sum;//完全包含
down(x);//只多了一个下传标记
int mid=(t[x].l+t[x].r)>>1;//区间中点
int sum=0;
if(mid>=l)sum+=ask(x*2,l,r);//访问左半部分
if(mid<r)sum+=ask(x*2+1,l,r);//访问右半部分
return sum;
}
区间查询就这样结束了,做做题练练手吧!
例题2
luogu P3372 【模板】线段树 1
这道题用线段树区间修改,区间查询就行了
点击查看题目
#include<bits/stdc++.h>
using namespace std;
int a[100010],n;
struct stree{
long long l,r;
long long sum,tag;
}t[400010];
void build(int x,int l,int r){
t[x].l=l,t[x].r=r;//传递区间[l,r]
if(l==r){
t[x].sum=a[l];
return;
}//此点如是叶子节点则结束递归
int mid=(l+r)>>1;//区间中点
build(x*2,l,mid);//构造左子树
build(x*2+1,mid+1,r);//构造右子树
t[x].sum=t[x*2].sum+t[x*2+1].sum;//从下往上传值
}
void down(int x){
if(t[x].tag){//如果有标记
t[x*2].tag+=t[x].tag;//下传左子树
t[x*2+1].tag+=t[x].tag;//下传右子树
t[x*2].sum+=(t[x*2].r-t[x*2].l+1)*t[x].tag;//左子树和增加
t[x*2+1].sum+=(t[x*2+1].r-t[x*2+1].l+1)*t[x].tag;//右子树和增加
t[x].tag=0;//清空标记
}
}
void change(int x,int l,int r,int a){
if(t[x].l>=l&&t[x].r<=r){//完全包含
t[x].tag+=a;//标记区间
t[x].sum+=(t[x].r-t[x].l+1)*a;//区间和增加
return;
}
if(t[x].l==t[x].r)return;//到叶子节点直接返回
down(x);//这时还没有操作就需下传标记
int mid=(t[x].l+t[x].r)>>1;//区间中点
if(mid>=l)change(x*2,l,r,a);//访问左半部分
if(mid<r)change(x*2+1,l,r,a);//访问右半部分
t[x].sum=t[x*2].sum+t[x*2+1].sum;//刷新值
}
long long ask(int x,int l,int r){
if(l<=t[x].l&&r>=t[x].r)return t[x].sum;//完全包含
down(x);//只多了一个下传标记
int mid=(t[x].l+t[x].r)>>1;//区间中点
long long sum=0;
if(mid>=l)sum+=ask(x*2,l,r);//访问左半部分
if(mid<r)sum+=ask(x*2+1,l,r);//访问右半部分
return sum;
}
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
}
build(1,1,n);//建树
while(m--){
string op;
int a,b,c;
cin>>op>>a>>b;
if(op=="1"){
cin>>c;
change(1,a,b,c);//区间修改
}else{
cout<<ask(1,a,b)<<endl;//区间查询
}
}
return 0;
}
提示:开long long
例题3
点击查看代码
顺序:加->乘->加
#include<bits/stdc++.h>
#define int long long
using namespace std;
int a[100010],n,p;
//线段树结构体,sum表示此时的答案,tagc表示乘法意义上的lazytag,tag是加法意义上的
struct stree{
long long l,r;
long long sum,tag,tagc;
}t[400010];
//buildtree
void build(int x,int l,int r){
t[x].tag=0;
t[x].tagc=1;
t[x].l=l,t[x].r=r;
if(l==r){
t[x].sum=a[l]%p;
return;
}
int mid=(l+r)>>1;
build(x*2,l,mid);
build(x*2+1,mid+1,r);
t[x].sum=(t[x*2].sum+t[x*2+1].sum)%p;
}
void down(int x){
t[x*2].sum=(t[x*2].sum*t[x].tagc%p+(t[x*2].r-t[x*2].l+1)*t[x].tag%p)%p;
t[x*2+1].sum=(t[x*2+1].sum*t[x].tagc%p+(t[x*2+1].r-t[x*2+1].l+1)*t[x].tag%p)%p;
t[x*2].tagc=t[x*2].tagc*t[x].tagc%p;
t[x*2+1].tagc=t[x*2+1].tagc*t[x].tagc%p;
t[x*2].tag=(t[x*2].tag*t[x].tagc%p+t[x].tag)%p;
t[x*2+1].tag=(t[x*2+1].tag*t[x].tagc%p+t[x].tag)%p;
t[x].tag=0;
t[x].tagc=1;
}
//加
void change(int x,int l,int r,int a){
if(r<t[x].l||t[x].r<l)return ;
if(t[x].l>=l&&t[x].r<=r){
t[x].tag=(t[x].tag+a%p)%p;
t[x].sum=(t[x].sum+(t[x].r-t[x].l+1)*a%p)%p;
return;
}
if(t[x].l==t[x].r)return;
down(x);
int mid=(t[x].l+t[x].r)>>1;
if(mid>=l)change(x*2,l,r,a);
if(mid<r)change(x*2+1,l,r,a);
t[x].sum=(t[x*2].sum+t[x*2+1].sum)%p;
}
//乘
void changech(int x,int l,int r,int a){
if(r<t[x].l||t[x].r<l)return;
if(t[x].l>=l&&t[x].r<=r){
t[x].tagc=t[x].tagc*a%p;
t[x].tag=t[x].tag*a%p;
t[x].sum=t[x].sum*a%p;
return;
}
if(t[x].l==t[x].r)return;
down(x);
int mid=(t[x].l+t[x].r)>>1;
if(mid>=l)changech(x*2,l,r,a);
if(mid<r)changech(x*2+1,l,r,a);
t[x].sum=(t[x*2].sum+t[x*2+1].sum)%p;
}
//访问
long long ask(int x,int l,int r){
if(r<t[x].l||t[x].r<l)return 0;
if(l<=t[x].l&&r>=t[x].r)return t[x].sum%p;
down(x);
int mid=(t[x].l+t[x].r)>>1;
long long sum=0;
if(mid>=l)sum+=ask(x*2,l,r)%p;
sum%=p;
if(mid<r)sum+=ask(x*2+1,l,r)%p;
sum%=p;
return sum;
}
signed main(){
//freopen("P3373_2.in","r",stdin);
//freopen("s.txt","w",stdout);
int n,m,k;
cin>>n>>m>>p;
for(int i=1;i<=n;i++){
cin>>a[i];
a[i]%=p;
}
build(1,1,n);
while(m--){
int op;
int a,b,c;
cin>>op>>a>>b;
if(op==1){
cin>>c;
changech(1,a,b,c);
}else if(op==2){
cin>>c;
change(1,a,b,c);
}else{
cout<<ask(1,a,b)%p<<endl;
}
}
return 0;
}