线段树学习笔记
线段树简介
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为 。而未优化的空间复杂度为 ,实际应用时一般还要开4N的数组以免越界,因此有时需要离散化让空间压缩。
——百度百科
线段树是一种基于分治思想的二叉树结构,用于区间信息统计
线段树对比树状数组有如下好处:
- 每个节点都代表一个区间
- 具有唯一根节点,代表
- 每个叶子节点代表一个元素
- 可以用二叉树的方式存储(详见下面)
因为线段树接近完全二叉树,故可以用如下表示方法:
- 根节点编号为
- 编号为 的节点左子节点编号为
- 编号为 的节点右子节点编号为
线段树特性:
- 长度是 的序列构造线段树,这颗线段树有 个节点(同二叉树叶子节点与所有节点数量的关系),高度为 。
- 存线段树的数组要开 倍空间!
线段树实现
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.单点修改与查询
线段树从根节点开始执行指令,我们可以通过递归找到要修改的节点,然后从下往上更新经过的所有节点(时间复杂度 )
我们如果更改点 ,需要更改的节点如图红圈部分:
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.区间查询(基础)
区间查询其实并不难,只要递归执行以下步骤:
- 若 完全覆盖了整个区间,立刻回溯
- 若左子节点与 有重叠部分,递归访问左子节点
- 若右子节点与 有重叠部分,递归访问右子节点
我们如果查询区间 ,需要查询的节点如图红圈部分:
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); }
如果我们更改区间 ,要更改的点如下图,其中红圈表示循环中直接更改的点,绿圈表示递归中要更改的点
是不是要更改很多点,这样做时间复杂度都退化到 了,比模拟法都慢
我们可以先不全修改,打上标记以后修改,但用的时候怎么办呢?我们可以到用的时候再更新,这样就可以把时间复杂度降到 了
如下图,我们只要修改红圈部分,并在完全覆盖的地方(绿圈部分)打上标记
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; }
线段树拓展
本文作者:ccrui
本文链接:https://www.cnblogs.com/ccr-note/p/stree.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步