线段树解题技巧

前言

线段树是一种在 log 时间内维护区间信息的数据结构,其维护的信息具有区间可加性。

区间可加性,也就是由区间 A 和区间 B,可以推出 AB

上面说到的区间,指的是区间内维护的信息。

如区间和,区间平方和,区间最值,区间最大子段,区间最长连续子段,这类问题就是具有区间可加性的。

关于线段树维护的题目,分为两类,一类是好维护的,一类是不好维护的,体现在修改与查询的关系并不大。下面分这两类进行分析。

好维护

好维护的信息通常是由修改可以推出查询,比如修改是将一个区间加上某个数,查询是查区间和,这时可以直接由修改推出查询。

P3373 【模板】线段树 2

比单纯的区间加稍微复杂一点。

这题显然是好维护的,对于一个区间加上一个数,很典,乘上一个数,考虑添加一个乘法懒标记。记 tag1 为加法标记,tag2 为乘法标记。

这时我们要考虑,加法标记和乘法标记的优先级。对于一个运算:

((x+1)×4+6)×7

不难发现,1 乘了 4×7,但是 6 只乘了 7,这提示我们不能直接将 tag1 累加至 sum,再用 tag2 去乘,此时我们将这个柿子的顺序变换一下:

x×4×7+1×4×7+6×7

这启示我们加法标记 tag1 存的实际是 1×4×7+6×7tag2 存的是 4×7,在最后计算 sum 时,采取先乘后加的方法。对于维护 tag 也是类似。

点击查看代码
#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 方差

对于平均数,这是很好维护的,只需维护区间和即可。

对于方差,我们利用高中数学知识将其化成如下形式:

s2=1ni=1n(AiA¯)2=1ni=1nAi2A¯2

对于这个玩意,维护区间平方和即可,考虑修改对查询的影响,若给alr+k,那么区间平方和为 (al+k)2+...+(ar+k)2al2+...+ar2+2k(al+...+ar)+(rl+1)×k2.

对于上面的式子,显然是好维护的,维护区间平方和,区间和,即可实现更新。

点击查看代码
#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] 脑洞治疗仪,但是这题求的是最长连续 0 的个数,和上文的最大子段和略有不同,注意区分。

「Wdsr-2.7」文文的摄影布置

比较有意思的线段树。

注意到要维护的式子是 Ai+Akmin(Bj)(i<j<k)max,考虑将式子拆成两个部分:AiAkmin(Bj) 或者 AkAimin(Bj) ,之所以将式子拆成两个部分是因为 i,jk,j 的相对顺序不一样。

维护是简单的,只需维护区间 Amax,区间 Bmin,区间 Aimin(Bj),区间 Akmin(Bj),区间 ans 即可。

点击查看代码
#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;
}

下面是非常规的题目,在线段树上维护差分数组。

区间最大公约数

口胡,没写代码。

如果直接维护每个区间的 gcd,那么在修改时无法实时更新区间的 gcd,毕竟区间 +kgcd 显然不是 +k

这是就要提到 gcd 的一个性质:

(a1,a2,...,an)=(a1,a2a1,...,anan1)

通过这个式子,我们发现,区间的 gcd 与其差分序列的 gcd 是相等的,所以我们考虑直接维护差分序列,这样对于区间加操作,转换为修改两个点的权值,在回溯时暴力更新 gcd 即可,时间复杂度 O(logn)

对于 (l,r) 的询问,也就是 (a[l],(b[l+1]...b[r])),其中 b 为差分数组,只需对于 a 再开一棵线段树即可。

总时间复杂度 O(mlogn)

不好维护

这类题通常不好维护,特征是询问很正常,但是修改却很奇怪,这类题目的通解便是——暴力修改,但同时修改会有性质,即一个点被修改的次数有限。

P4145 上帝造题的七分钟 2 / 花神游历各国

相当经典的题目,询问区间和,修改为开方。

对于开方,显然没有太好的处理办法,毕竟原来的区间和是 sum,将区间内每一个数字开方后,区间和不是 sum,所以根本没法用懒标记维护。

但是我们考虑一个点最多会被开方几次,极限情况是 1012,被开方 6 次就到 1,到 1 以后在开方值也不会变,利用这个性质,可以在每个节点记录该区间内是否全部数字都是 1,如果是,就不用修改;否则一个一个暴力修改。

时间复杂度O(6mlogn)

点击查看代码
#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 位补码取按位或。

这句话是让我们用 int 去按位或,若用LL,达不到补码的要求。

按位或有一个很好的性质:只会增大,不会减小。所以只要分析一个数最多被修改几次即可。

对于一个有效的修改,至少会将 a 的一位从 0 变成 1,而a 至多 30 位,所以最多会处理 30 次。

对于一个区间,如何判读修改的 k 是不是有效修改呢,若改区间内所有数字都包含 k 的二进制位,显然是无效的,所以维护区间所有数字的按位并,记作 x,若 xk=k,即可说明所有数字都包含 k,即为无效修改。对于有效修改,暴力修改即可。

时间复杂度 O(30mlogn)

注意,本题的子段和可以不选。

点击查看代码
#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

同样考虑一个数最多模几次。

考虑一个数 x,若 xmodyyx2,那么结果小于 x2,若 yx2,结果也会小于 x2

所以,一个数字最多模 loga 次,维护区间是否全部为 1,不是则直接暴力修改即可。

时间复杂度 O(mlogalogn)

点击查看代码
#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;
}
posted @   2017BeiJiang  阅读(18)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示