线段树解题技巧
前言
线段树是一种在 时间内维护区间信息的数据结构,其维护的信息具有区间可加性。
区间可加性,也就是由区间 和区间 ,可以推出 。
上面说到的区间,指的是区间内维护的信息。
如区间和,区间平方和,区间最值,区间最大子段,区间最长连续子段,这类问题就是具有区间可加性的。
关于线段树维护的题目,分为两类,一类是好维护的,一类是不好维护的,体现在修改与查询的关系并不大。下面分这两类进行分析。
好维护
好维护的信息通常是由修改可以推出查询,比如修改是将一个区间加上某个数,查询是查区间和,这时可以直接由修改推出查询。
P3373 【模板】线段树 2
比单纯的区间加稍微复杂一点。
这题显然是好维护的,对于一个区间加上一个数,很典,乘上一个数,考虑添加一个乘法懒标记。记 为加法标记, 为乘法标记。
这时我们要考虑,加法标记和乘法标记的优先级。对于一个运算:
不难发现, 乘了 ,但是 只乘了 ,这提示我们不能直接将 累加至 ,再用 去乘,此时我们将这个柿子的顺序变换一下:
这启示我们加法标记 存的实际是 , 存的是 ,在最后计算 时,采取先乘后加的方法。对于维护 也是类似。
点击查看代码
#include<bits/stdc++.h> using namespace std; typedef long long LL; typedef unsigned long long ULL; LL read() { LL sum=0,flag=1; char c=getchar(); while(c<'0'||c>'9') {if(c=='-') flag=-1; c=getchar();} while(c>='0'&&c<='9') {sum=sum*10+c-'0'; c=getchar();} return sum*flag; } const int N=1e5+10; int n,q,m; LL tr[N<<2],tag1[N<<2],tag2[N<<2]; void add(int nd,int l,int r,LL x1,LL x2) { tag1[nd]=(tag1[nd]*x2+x1)%m; tag2[nd]=(tag2[nd]*x2)%m; tr[nd]=(tr[nd]*x2%m+(r-l+1)*x1%m)%m; } void pushdown(int nd,int l,int r) { int mid=l+r>>1; add(nd<<1,l,mid,tag1[nd],tag2[nd]); add(nd<<1|1,mid+1,r,tag1[nd],tag2[nd]); tag1[nd]=0; tag2[nd]=1; } void pushup(int nd) { tr[nd]=(tr[nd<<1]+tr[nd<<1|1])%m; } void change(int nd,int l,int r,int x,int y,LL x1,LL x2) { if(r<x||l>y) return ; if(l>=x&&r<=y) return add(nd,l,r,x1,x2); pushdown(nd,l,r); int mid=l+r>>1; change(nd<<1,l,mid,x,y,x1,x2); change(nd<<1|1,mid+1,r,x,y,x1,x2); pushup(nd); } LL ask(int nd,int l,int r,int x,int y) { if(r<x||l>y) return 0; if(l>=x&&r<=y) return tr[nd]; pushdown(nd,l,r); int mid=l+r>>1; return (ask(nd<<1,l,mid,x,y)+ask(nd<<1|1,mid+1,r,x,y))%m; } int main() { // freopen("a.in","r",stdin); // freopen("a.out","w",stdout); n=read(); q=read(); m=read(); for(int i=1;i<=n*4;i++) { tag1[i]=0; tag2[i]=1; } for(int i=1;i<=n;i++) { LL x=read(); change(1,1,n,i,i,x,1); } while(q--) { int opt=read(),x=read(),y=read(); LL k; if(opt==1) { k=read(); change(1,1,n,x,y,0,k); } else if(opt==2) { k=read(); change(1,1,n,x,y,k,1); } else { cout<<ask(1,1,n,x,y)<<'\n'; } } return 0; }
P1471 方差
对于平均数,这是很好维护的,只需维护区间和即可。
对于方差,我们利用高中数学知识将其化成如下形式:
对于这个玩意,维护区间平方和即可,考虑修改对查询的影响,若给,那么区间平方和为 即 .
对于上面的式子,显然是好维护的,维护区间平方和,区间和,即可实现更新。
点击查看代码
#include<bits/stdc++.h> using namespace std; typedef long long LL; typedef unsigned long long ULL; LL read() { LL sum=0,flag=1; char c=getchar(); while(c<'0'||c>'9') {if(c=='-') flag=-1; c=getchar();} while(c>='0'&&c<='9') {sum=sum*10+c-'0'; c=getchar();} return sum*flag; } const int N=1e5+10; int n,m; double sum1[N<<2],sum2[N<<2],tag[N<<2]; struct node { double s1,s2; }; void add(int nd,int l,int r,double k) { tag[nd]+=k; sum2[nd]=sum2[nd]+2*k*sum1[nd]+(double)(r-l+1)*k*k; sum1[nd]+=(r-l+1)*k; } void pushdown(int nd,int l,int r) { int mid=l+r>>1; if(!tag[nd]) return ; add(nd<<1,l,mid,tag[nd]); add(nd<<1|1,mid+1,r,tag[nd]); tag[nd]=0; } void pushup(int nd) { sum1[nd]=sum1[nd<<1]+sum1[nd<<1|1]; sum2[nd]=sum2[nd<<1]+sum2[nd<<1|1]; } void change(int nd,int l,int r,int x,int y,double k) { if(r<x||l>y) return ; if(l>=x&&r<=y) return add(nd,l,r,k); int mid=l+r>>1; pushdown(nd,l,r); change(nd<<1,l,mid,x,y,k); change(nd<<1|1,mid+1,r,x,y,k); pushup(nd); } node query(int nd,int l,int r,int x,int y) { if(r<x||l>y) return {0,0}; if(l>=x&&r<=y) return {sum1[nd],sum2[nd]}; pushdown(nd,l,r); int mid=l+r>>1; node x1=query(nd<<1,l,mid,x,y); node x2=query(nd<<1|1,mid+1,r,x,y); return {x1.s1+x2.s1,x1.s2+x2.s2}; } int main() { // freopen("a.in","r",stdin); // freopen("a.out","w",stdout); n=read(); m=read(); for(int i=1;i<=n;i++) { double x; cin>>x; change(1,1,n,i,i,x); } while(m--) { int opt=read(),x=read(),y=read(); double k; if(opt==1) { cin>>k; change(1,1,n,x,y,k); } else { node ans=query(1,1,n,x,y); if(opt==2) { printf("%.4lf\n",ans.s1*1.0/(y*1.0-x*1.0+1.0)); } else { double avg=ans.s1*1.0/((y-x+1)*1.0); double kkk=ans.s2*1.0/((y-x+1)*1.0); printf("%.4lf\n",kkk-avg*avg); } } } return 0; }
P4513 小白逛公园
维护最大子段和的板子题。
考虑将两个区间拼在一起如何更新答案。
可以考虑维护一个区间左边连续的最大值,右边连续的最大值。那么将新区间的最大子段和可以由左右区间的答案构成,也可以有左区间的右边最大值,加上右区间的左边最大值加起来,取最大值即可。
左右的连续最大值是好求的,具体看代码。
点击查看代码
#include<bits/stdc++.h> using namespace std; typedef long long LL; typedef unsigned long long ULL; LL read() { LL sum=0,flag=1; char c=getchar(); while(c<'0'||c>'9') {if(c=='-') flag=-1; c=getchar();} while(c>='0'&&c<='9') {sum=sum*10+c-'0'; c=getchar();} return sum*flag; } const int N=5e5+10; int n,m; struct node { int sum,maxn,lmax,rmax; }tr[N<<2]; node merge(node x,node y) { node k; k.sum=x.sum+y.sum; k.lmax=max(x.lmax,x.sum+y.lmax); k.rmax=max(y.rmax,x.rmax+y.sum); k.maxn=max(max(x.maxn,y.maxn),x.rmax+y.lmax); return k; } void change(int nd,int l,int r,int p,int k) { if(r<p||l>p) return ; if(l==r&&l==p) { tr[nd].sum=tr[nd].lmax=tr[nd].rmax=tr[nd].maxn=k; return ; } int mid=l+r>>1; change(nd<<1,l,mid,p,k); change(nd<<1|1,mid+1,r,p,k); tr[nd]=merge(tr[nd<<1],tr[nd<<1|1]); } node query(int nd,int l,int r,int x,int y) { if(l>=x&&r<=y) return tr[nd]; int mid=l+r>>1; node s; int flag=0; if(mid>=x) { node k=query(nd<<1,l,mid,x,y); s=k;flag=1; } if(mid+1<=y) { node k=query(nd<<1|1,mid+1,r,x,y); if(!flag) s=k; else s=merge(s,k); } return s; } int main() { // freopen("a.in","r",stdin); // freopen("a.out","w",stdout); n=read(); m=read(); for(int i=1;i<=n;i++) { int x=read(); change(1,1,n,i,x); } while(m--) { int k=read(),a=read(),b=read(); if(k==1) { if(a>b) swap(a,b); node x=query(1,1,n,a,b); cout<<x.maxn<<'\n'; } else { change(1,1,n,a,b); } } return 0; }
类似问题:[SHOI2015] 脑洞治疗仪,但是这题求的是最长连续 的个数,和上文的最大子段和略有不同,注意区分。
「Wdsr-2.7」文文的摄影布置
比较有意思的线段树。
注意到要维护的式子是 的 ,考虑将式子拆成两个部分: 和 或者 和 ,之所以将式子拆成两个部分是因为 与 的相对顺序不一样。
维护是简单的,只需维护区间 ,区间 ,区间 ,区间 ,区间 即可。
点击查看代码
#include<bits/stdc++.h> using namespace std; typedef long long LL; typedef unsigned long long ULL; LL read() { LL sum=0,flag=1; char c=getchar(); while(c<'0'||c>'9') {if(c=='-') flag=-1; c=getchar();} while(c>='0'&&c<='9') {sum=sum*10+c-'0'; c=getchar();} return sum*flag; } const int N=5e5+10; const int INF=1e9; int n,m; int a[N],b[N]; struct node { int ans,cij,ckj,maxa,minb; }tr[N<<2]; node merge(node x,node y) { node k; k.maxa=max(x.maxa,y.maxa); k.minb=min(x.minb,y.minb); k.cij=max(max(x.cij,y.cij),x.maxa-y.minb); k.ckj=max(max(x.ckj,y.ckj),y.maxa-x.minb); k.ans=max(max(x.ans,y.ans),max(x.maxa+y.ckj,x.cij+y.maxa)); return k; } void change(int nd,int l,int r,int x,int a,int b) { if(r<x||l>x) return ; if(l==r) { tr[nd].maxa=a; tr[nd].minb=b; tr[nd].ans=tr[nd].cij=tr[nd].ckj=-INF; return ; } int mid=l+r>>1; change(nd<<1,l,mid,x,a,b); change(nd<<1|1,mid+1,r,x,a,b); tr[nd]=merge(tr[nd<<1],tr[nd<<1|1]); } node query(int nd,int l,int r,int x,int y) { if(l>=x&&r<=y) return tr[nd]; int mid=l+r>>1; node s; int flag=0; if(mid>=x) { node k=query(nd<<1,l,mid,x,y); s=k; flag=1; } if(mid+1<=y) { node k=query(nd<<1|1,mid+1,r,x,y); if(flag) s=merge(s,k); else s=k; } return s; } int main() { n=read(); m=read(); for(int i=1;i<=n;i++) a[i]=read(); for(int i=1;i<=n;i++) b[i]=read(); for(int i=1;i<=n;i++) { change(1,1,n,i,a[i],b[i]); } while(m--) { int opt=read(),x=read(),y=read(); if(opt==1) { a[x]=y; change(1,1,n,x,a[x],b[x]); } else if(opt==2) { b[x]=y; change(1,1,n,x,a[x],b[x]); } else { cout<<query(1,1,n,x,y).ans<<'\n'; } } return 0; }
下面是非常规的题目,在线段树上维护差分数组。
区间最大公约数
口胡,没写代码。
如果直接维护每个区间的 ,那么在修改时无法实时更新区间的 ,毕竟区间 , 显然不是 。
这是就要提到 的一个性质:
通过这个式子,我们发现,区间的 与其差分序列的 是相等的,所以我们考虑直接维护差分序列,这样对于区间加操作,转换为修改两个点的权值,在回溯时暴力更新 即可,时间复杂度 。
对于 的询问,也就是 ,其中 为差分数组,只需对于 再开一棵线段树即可。
总时间复杂度 。
不好维护
这类题通常不好维护,特征是询问很正常,但是修改却很奇怪,这类题目的通解便是——暴力修改,但同时修改会有性质,即一个点被修改的次数有限。
P4145 上帝造题的七分钟 2 / 花神游历各国
相当经典的题目,询问区间和,修改为开方。
对于开方,显然没有太好的处理办法,毕竟原来的区间和是 ,将区间内每一个数字开方后,区间和不是 ,所以根本没法用懒标记维护。
但是我们考虑一个点最多会被开方几次,极限情况是 ,被开方 次就到 ,到 以后在开方值也不会变,利用这个性质,可以在每个节点记录该区间内是否全部数字都是 ,如果是,就不用修改;否则一个一个暴力修改。
时间复杂度
点击查看代码
#include<bits/stdc++.h> using namespace std; typedef long long LL; typedef unsigned long long ULL; LL read() { LL sum=0,flag=1; char c=getchar(); while(c<'0'||c>'9') {if(c=='-') flag=-1; c=getchar();} while(c>='0'&&c<='9') {sum=sum*10+c-'0'; c=getchar();} return sum*flag; } const int N=1e5+10; int n,m; LL a[N]; struct node { LL val; int cnt; }tr[N<<2]; void build(int nd,int l,int r) { if(l==r) { tr[nd].val=a[l]; if(a[l]==1) tr[nd].cnt=1; return ; } int mid=l+r>>1; build(nd<<1,l,mid); build(nd<<1|1,mid+1,r); tr[nd].cnt=tr[nd<<1].cnt+tr[nd<<1|1].cnt; tr[nd].val=tr[nd<<1].val+tr[nd<<1|1].val; } void change(int nd,int l,int r,int x,int y) { if(r<x||l>y) return ; if(l==r) { tr[nd].val=sqrt(tr[nd].val); if(tr[nd].val==1) tr[nd].cnt=1; return ; } int mid=l+r>>1; if(tr[nd<<1].cnt!=mid-l+1) change(nd<<1,l,mid,x,y); if(tr[nd<<1|1].cnt!=r-mid) change(nd<<1|1,mid+1,r,x,y); tr[nd].cnt=tr[nd<<1].cnt+tr[nd<<1|1].cnt; tr[nd].val=tr[nd<<1].val+tr[nd<<1|1].val; } LL query(int nd,int l,int r,int x,int y) { if(r<x||l>y) return 0; if(l>=x&&r<=y) return tr[nd].val; int mid=l+r>>1; return query(nd<<1,l,mid,x,y)+query(nd<<1|1,mid+1,r,x,y); } int main() { n=read(); for(int i=1;i<=n;i++) a[i]=read(); build(1,1,n); m=read(); while(m--) { int k=read(),l=read(),r=read(); if(l>r) swap(l,r); if(!k) { change(1,1,n,l,r); } else { cout<<query(1,1,n,l,r)<<'\n'; } } return 0; }
P7492 [传智杯 #3 决赛] 序列
注意:负数按照 32 位补码取按位或。
这句话是让我们用 去按位或,若用,达不到补码的要求。
按位或有一个很好的性质:只会增大,不会减小。所以只要分析一个数最多被修改几次即可。
对于一个有效的修改,至少会将 的一位从 变成 ,而 至多 位,所以最多会处理 次。
对于一个区间,如何判读修改的 是不是有效修改呢,若改区间内所有数字都包含 的二进制位,显然是无效的,所以维护区间所有数字的按位并,记作 ,若 ,即可说明所有数字都包含 ,即为无效修改。对于有效修改,暴力修改即可。
时间复杂度 。
注意,本题的子段和可以不选。
点击查看代码
#include<bits/stdc++.h> using namespace std; typedef long long LL; typedef unsigned long long ULL; LL read() { LL sum=0,flag=1; char c=getchar(); while(c<'0'||c>'9') {if(c=='-') flag=-1; c=getchar();} while(c>='0'&&c<='9') {sum=sum*10+c-'0'; c=getchar();} return sum*flag; } const int N=1e5+10; int n,m; int a[N]; struct node { LL maxn,lmax,rmax,sum; int val; }tr[N<<2]; node merge(node x,node y) { node k; k.sum=x.sum+y.sum; k.lmax=max(x.lmax,x.sum+y.lmax); k.rmax=max(y.rmax,y.sum+x.rmax); k.maxn=max(max(x.maxn,y.maxn),x.rmax+y.lmax); k.val=(x.val&y.val); return k; } void build(int nd,int l,int r) { if(l==r) { tr[nd].val=a[l]; tr[nd].lmax=tr[nd].rmax=tr[nd].maxn=tr[nd].sum=a[l]; return ; } int mid=l+r>>1; build(nd<<1,l,mid); build(nd<<1|1,mid+1,r); tr[nd]=merge(tr[nd<<1],tr[nd<<1|1]); } void change(int nd,int l,int r,int x,int y,int k) { if(r<x||l>y) return ; if(l==r) { tr[nd].val=(tr[nd].val | k); tr[nd].lmax=tr[nd].maxn=tr[nd].rmax=tr[nd].sum=tr[nd].val; return ; } int mid=l+r>>1; if((tr[nd<<1].val&k)!=k) change(nd<<1,l,mid,x,y,k); if((tr[nd<<1|1].val&k)!=k) change(nd<<1|1,mid+1,r,x,y,k); tr[nd]=merge(tr[nd<<1],tr[nd<<1|1]); } node query(int nd,int l,int r,int x,int y) { if(l>=x&&r<=y) return tr[nd]; int mid=l+r>>1,flag=0; node s; if(mid>=x) { node k=query(nd<<1,l,mid,x,y); flag=1; s=k; } if(mid+1<=y) { node k=query(nd<<1|1,mid+1,r,x,y); if(!flag) s=k; else s=merge(s,k); } return s; } int main() { n=read(); m=read(); for(int i=1;i<=n;i++) { a[i]=read(); } build(1,1,n); while(m--) { int op=read(),l=read(),r=read(),k; if(op==1) { node ans=query(1,1,n,l,r); cout<<max((LL)0,ans.maxn)<<'\n'; } else { k=read(); change(1,1,n,l,r,k); } } return 0; }
CF438D The Child and Sequence
同样考虑一个数最多模几次。
考虑一个数 ,若 且 ,那么结果小于 ,若 ,结果也会小于 。
所以,一个数字最多模 次,维护区间是否全部为 ,不是则直接暴力修改即可。
时间复杂度 。
点击查看代码
#include<bits/stdc++.h> using namespace std; typedef long long LL; typedef unsigned long long ULL; LL read() { LL sum=0,flag=1; char c=getchar(); while(c<'0'||c>'9') {if(c=='-') flag=-1; c=getchar();} while(c>='0'&&c<='9') {sum=sum*10+c-'0'; c=getchar();} return sum*flag; } const int N=1e5+10; int n,m; struct node{ LL sum,maxn; }tr[N<<2]; void pushup(int nd) { tr[nd].sum=tr[nd<<1].sum+tr[nd<<1|1].sum; tr[nd].maxn=max(tr[nd<<1].maxn,tr[nd<<1|1].maxn); } void change1(int nd,int l,int r,int x,int k) { if(l>x||r<x) return ; if(l==r) { tr[nd].sum=k; tr[nd].maxn=k; return ; } int mid=l+r>>1; change1(nd<<1,l,mid,x,k); change1(nd<<1|1,mid+1,r,x,k); pushup(nd); } void change2(int nd,int l,int r,int x,int y,int k) { if(l>y||r<x) return ; if(l==r) { tr[nd].sum%=k; tr[nd].maxn%=k; return ; } int mid=l+r>>1; if(tr[nd<<1].maxn>=k) change2(nd<<1,l,mid,x,y,k); if(tr[nd<<1|1].maxn>=k) change2(nd<<1|1,mid+1,r,x,y,k); pushup(nd); } LL query(int nd,int l,int r,int x,int y) { if(r<x||l>y) return 0; if(l>=x&&r<=y) return tr[nd].sum; int mid=l+r>>1; return query(nd<<1,l,mid,x,y)+query(nd<<1|1,mid+1,r,x,y); } int main() { n=read(); m=read(); for(int i=1;i<=n;i++) { int x=read(); change1(1,1,n,i,x); } while(m--) { int opt=read(),l=read(),r=read(),x; if(opt==1) { cout<<query(1,1,n,l,r)<<'\n'; } else if(opt==2) { x=read(); change2(1,1,n,l,r,x); } else { change1(1,1,n,l,r); } } return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效