线段树学习笔记
线段树的基础知识
什么是线段树
线段树是一种分治思想的二叉树结构,用于在区间上进行信息维护与统计,与按照二进制进行区间划分的树状数组相比,线段树是一种更为通用的数据结构:
- 线段树的每一个节点都代表一个区间。
- 线段树有唯一的根节点,代表的区间是整个统计的范围。
- 线段树的每一个叶子节点都代表一个长度为
的元区间 。 - 定义对于每个内部节点
,定义 ,它的左子节点为 ,右子节点为 。
除去线段树的最后一层,整棵线段树一定是一棵完全二叉树,深度为
- 根节点编号为
。 - 编号为
的节点的左子节点编号为 ,右子节点编号为 。
在理想情况下, 个叶节点的满二叉树有 个节点。应为在上述储存方式下,最后还有一层产生了空余,所以保存线段树的数组长度不应少于 。
线段树的基本操作
线段树的建树
给定一个长度为
int a[100005]; struct node{ int l,r,date; #define l(x) t[x].l; #define r(x) t[x].r; #define date(x) t[x].date; }t[100005*4]; void build(int p,int l,int r){ l(p)=l; r(p)=r; if(l==r){ date(p)=a[l]; return; } int mid=(l+r)/2; build(p*2,l,mid); build(p*2+!,mid+1,r); date(p)=max(date(p*2),date(p*2+1)); }
线段树的单点修改
将
void change(int p,int x,int v){ if(l(p)==r(p)){ date(p)=v; return; } int mid=(l(p)+r(p))/2; if(x<=mid) change(p*2,x,v); else change(p*2+1,x,v); date(p)=max(date(p*2),date(p*2+1)); }
线段树的区间查询
查询序列
- 若
完全覆盖了当前节点代表的区间,立即回溯,并且该节点的 值为候选项。 - 若左子节点与
有重叠部分,递归访问左子节点。 - 若右子节点与
有重叠部分,递归访问右子节点。
int ask(int p,int l,int r){ if(l<=l(p) && r>=r(p)) return date(p); int mid=(l(p)+r(p))/2,v=-(1<<30);//负无穷大 if(l<=mid) v=max(v,ask(p*2,l,r)); if(r>mid) v=max(v,ask(p*2+1,l,r)); return v; }
线段树的区间修改
在区间修改操作中,如果某个节点被修改区间
假设我们逐一修改了被查询区间完全覆盖的节点
这启发我们在后续修改操作中,同样也可以在修改区间完全覆盖当前节点代表的区间时立即返回,但是在回溯之前在
在后续的指令中,需要从节点
区间查询,区间修改的时间复杂度均为
模板
以【模板】线段树1为例
#include<bits/stdc++.h> using namespace std; struct node{ int l,r; long long sum,add; #define l(x) tree[x].l #define r(x) tree[x].r #define sum(x) tree[x].sum #define add(x) tree[x].add }tree[100005*4]; int n,m,t1,t2,t3,t4,a[100005]; void build(int p,int l,int r){ l(p)=l; r(p)=r; if(l==r){ sum(p)=a[l]; return; } int mid=(l+r)/2; build(p*2,l,mid); build(p*2+1,mid+1,r); sum(p)=sum(p*2)+sum(p*2+1); } void spread(int p){ if(add(p)){ sum(p*2)+=add(p)*(r(p*2)-l(p*2)+1); sum(p*2+1)+=add(p)*(r(p*2+1)-l(p*2+1)+1); add(p*2)+=add(p); add(p*2+1)+=add(p); add(p)=0; } } void change(int p,int l,int r,int d){ if(l<=l(p) && r>=r(p)){ sum(p)+=(long long)d*(r(p)-l(p)+1); add(p)+=d; return; } spread(p); int mid=(l(p)+r(p))/2; if(l<=mid) change(p*2,l,r,d); if(r>mid) change(p*2+1,l,r,d); sum(p)=sum(p*2)+sum(p*2+1); } long long ask(int p,int l,int r){ if(l<=l(p) && r>=r(p)) return sum(p); spread(p); int mid=(l(p)+r(p))/2; long long v=0; if(l<=mid) v+=ask(p*2,l,r); if(r>mid) v+=ask(p*2+1,l,r); return v; } int main(){ cin>>n>>m; for(int i=1;i<=n;i++) cin>>a[i]; build(1,1,n); while(m--){ cin>>t1>>t2>>t3; if(t1==1){ cin>>t4; change(1,t2,t3,t4); } else{ cout<<ask(1,t2,t3)<<endl; } } return 0; }
动态开点
普通的线段树需要开
代码如下:
#include<bits/stdc++.h> using namespace std; struct node{ int son_l,son_r; long long sum,tag; #define son_l(x) t[x].son_l #define son_r(x) t[x].son_r #define sum(x) t[x].sum #define tag(x) t[x].tag }t[100005*2]; int tot=1,root=1,n,m; void add(int &p,int l,int r,long long d){ if(!p) p=++tot; sum(p)+=(r-l+1)*d; tag(p)+=d; } void spread(int p,int l,int r){ if(l>=r) return; int mid=(l+r)>>1; add(son_l(p),l,mid,tag(p)); add(son_r(p),mid+1,r,tag(p)); tag(p)=0; } void change(int p,int l_now,int r_now,int l_to,int r_to,int d){ if(l_to<=l_now && r_to>=r_now){ add(p,l_now,r_now,d); return; } spread(p,l_now,r_now); int mid=(l_now+r_now)>>1; if(l_to<=mid) change(son_l(p),l_now,mid,l_to,r_to,d); if(r_to>mid) change(son_r(p),mid+1,r_now,l_to,r_to,d); sum(p)=sum(son_l(p))+sum(son_r(p)); } long long ask(int p,int l_now,int r_now,int l_to,int r_to){ if(l_to<=l_now && r_to>=r_now){ return sum(p); } spread(p,l_now,r_now); int mid=(l_now+r_now)>>1; long long val=0; if(l_to<=mid) val+=ask(son_l(p),l_now,mid,l_to,r_to); if(r_to>mid) val+=ask(son_r(p),mid+1,r_now,l_to,r_to); return val; } int main(){ cin>>n>>m; for(int i=1;i<=n;i++){ int t; cin>>t; change(root,1,n,i,i,t); } while(m--){ int t1,t2,t3,t4; cin>>t1>>t2>>t3; if(t1==1){ cin>>t4; change(root,1,n,t2,t3,t4); } else{ cout<<ask(root,1,n,t2,t3)<<endl; } } return 0; }
可持久化线段树
以上,我们讨论的线段树都只能解决对当前数据进行操作的问题,如果想解决对任意时间下的数据进行操作的问题,我们就需要对线段树进行可持久化的改造。
以线段树的单点修改,区间查询最大值为例,单点修改操作只会使
需要注意的是,经过这样改造的线段树将不再是一颗完全二叉树,所以我们不能沿用父子二倍编号法。
模板题。
代码如下:
#include<bits/stdc++.h> using namespace std; inline int read(){register int t1=0,t2=0;register char x=getchar();while(x<'0' ||x>'9'){if(x=='-') t2|=1;x=getchar();}while(x>='0' && x<='9'){t1=(t1<<1)+(t1<<3)+(x^48),x=getchar();}return t2?-t1:t1;} inline void write(int x){register int sta[35],top=0;if(x<0) putchar('-'),x=-x;do{sta[top++]=x%10,x/=10;}while(x);while(top) putchar(sta[--top]+48);} int n,m,a[1000005],root[1000005],tot; struct node{ int l,r,data; }t[1000005<<5]; int build(int l,int r){ int p=++tot; if(l==r){ t[p].data=a[l]; return p; } int mid=l+r>>1; t[p].l=build(l,mid); t[p].r=build(mid+1,r); return p; } inline int change(int now,int l,int r,int x,int val){ int p=++tot; t[p]=t[now]; if(l==r){ t[p].data=val; return p; } int mid=l+r>>1; if(x<=mid) t[p].l=change(t[now].l,l,mid,x,val); else t[p].r=change(t[now].r,mid+1,r,x,val); return p; } inline int ask(int now,int l,int r,int x){ if(l==r) return t[now].data; int mid=l+r>>1; if(x<=mid) return ask(t[now].l,l,mid,x); else return ask(t[now].r,mid+1,r,x); } int main(){ n=read(); m=read(); for(int i=1;i<=n;i++){ a[i]=read(); } root[0]=build(1,n); for(int i=1;i<=m;i++){ int t1=read(),t2=read(),t3=read(); if(t2==1){ int t4=read(); root[i]=change(root[t1],1,n,t3,t4); } else{ write(ask(root[t1],1,n,t3)); putchar('\n'); root[i]=root[t1]; } } return 0; }
一些例题
The Child and Sequence
题目链接。
解法分析
设模数为
Code
#include<bits/stdc++.h> using namespace std; int n,m,a[100005]; struct node{ int l,r; long long sum,mx; #define l(x) t[x].l #define r(x) t[x].r #define sum(x) t[x].sum #define mx(x) t[x].mx }t[100005*4]; void build(int p,int l,int r){ l(p)=l; r(p)=r; if(l==r){ sum(p)=a[l]; mx(p)=a[l]; return; } int mid=(l+r)/2; build(p*2,l,mid); build(p*2+1,mid+1,r); sum(p)=sum(p*2)+sum(p*2+1); mx(p)=max(mx(p*2),mx(p*2+1)); } void change(int p,int x,long long v){ if(l(p)==r(p)){ sum(p)=v; mx(p)=v; return; } int mid=(l(p)+r(p))/2; if(x<=mid) change(p*2,x,v); else change(p*2+1,x,v); mx(p)=max(mx(p*2),mx(p*2+1)); sum(p)=sum(p*2+1)+sum(p*2); } void change_mod(int p,int l,int r,long long d){ if(l<=l(p) && r>=r(p) && mx(p)<d) return; if(l<=l(p) && r>=r(p) && l(p)==r(p)){ mx(p)%=d; sum(p)%=d; return; } int mid=(l(p)+r(p))/2; if(l<=mid) change_mod(p*2,l,r,d); if(r>mid) change_mod(p*2+1,l,r,d); sum(p)=sum(p*2)+sum(p*2+1); mx(p)=max(mx(p*2),mx(p*2+1)); } long long ask(int p,int l,int r){ if(l<=l(p) && r>=r(p)) return sum(p); int mid=(l(p)+r(p))/2; long long v=0; if(l<=mid) v+=ask(p*2,l,r); if(r>mid) v+=ask(p*2+1,l,r); return v; } int main(){ cin>>n>>m; for(int i=1;i<=n;i++){ cin>>a[i]; } build(1,1,n); while(m--){ int t1; cin>>t1; if(t1==1){ int t2,t3; cin>>t2>>t3; cout<<ask(1,t2,t3)<<endl; } else if(t1==2){ int t2,t3,t4; cin>>t2>>t3>>t4; change_mod(1,t2,t3,t4); } else if(t1==3){ int t2,t3; cin>>t2>>t3; change(1,t2,t3); } } return 0; }
Legacy
题目链接。
相关知识:线段树优化建图
注:图片来自@123wwm。
建立两棵线段树,一棵为“入树”,一棵为“出树”,树中的叶子节点即为图中的节点。
在图中代表相同的节点的出树与入树上的点需要连接一条权值为
在进行区间向点连边或点向区间连边的操作时,在代表这一区间的出树上的节点与需要连边的在入树上的叶子节点之间连一条有向边。
解法分析
使用线段树优化建图之后,跑一遍 dijkstra 算法求最短路即可。
Code
#include<bits/stdc++.h> using namespace std; int n,Q,s,a[100005],k; long long dis[1000005]; struct tree{ int l,r; #define l(x) t[x].l #define r(x) t[x].r }t[100005*4]; struct node{ int to; long long l; }; vector<node> v[1000005]; struct node1{ int now; long long sum; }; bool operator <(const node1 &x,const node1 &y){ return x.sum>y.sum; } priority_queue<node1> q; bool vis[1000005]; void build(int p,int l,int r){ l(p)=l; r(p)=r; if(l==r){ a[l]=p; return; } v[p].push_back((node){p<<1,0}); v[p].push_back((node){p<<1|1,0}); v[(p<<1)+k].push_back((node){p+k,0}); v[(p<<1|1)+k].push_back((node){p+k,0}); int mid=(l+r)>>1; build(p<<1,l,mid); build(p<<1|1,mid+1,r); } void change(int p,int l,int r,int to,int lo,int m){ if(l<=l(p) && r>=r(p)){ if(m==3) v[p+k].push_back((node){to,lo}); else v[to+k].push_back((node){p,lo}); return; } int mid=(l(p)+r(p))>>1; if(l<=mid) change(p<<1,l,r,to,lo,m); if(r>mid) change(p<<1|1,l,r,to,lo,m); } void dijkstra(){ memset(dis,0x3f,sizeof(dis)); dis[a[s]+k]=0; q.push((node1){a[s]+k,0}); while(!q.empty()){ node1 x=q.top(); q.pop(); if(vis[x.now]) continue; vis[x.now]=1; for(int i=0;i<v[x.now].size();i++){ node net=v[x.now][i]; if(dis[net.to]>dis[x.now]+net.l){ dis[net.to]=dis[x.now]+net.l; q.push((node1){net.to,dis[net.to]}); } } } } int main(){ cin>>n>>Q>>s; k=5e5; build(1,1,n); for(int i=1;i<=n;i++){ v[a[i]].push_back((node){a[i]+k,0}); v[a[i]+k].push_back((node){a[i],0}); } while(Q--){ int t1,t2,t3,t4,t5; cin>>t1>>t2>>t3>>t4; if(t1==1) v[a[t2]+k].push_back((node){a[t3],t4}); else{ cin>>t5; change(1,t3,t4,a[t2],t5,t1); } } dijkstra(); for(int i=1;i<=n;i++){ if(dis[a[i]]==0x3f3f3f3f3f3f3f3fll) cout<<-1<<" "; else cout<<dis[a[i]]<<" "; } return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具