线段树解题技巧
前言
线段树是一种在 \(\log\) 时间内维护区间信息的数据结构,其维护的信息具有区间可加性。
区间可加性,也就是由区间 \(A\) 和区间 \(B\),可以推出 \(A\cup B\)。
上面说到的区间,指的是区间内维护的信息。
如区间和,区间平方和,区间最值,区间最大子段,区间最长连续子段,这类问题就是具有区间可加性的。
关于线段树维护的题目,分为两类,一类是好维护的,一类是不好维护的,体现在修改与查询的关系并不大。下面分这两类进行分析。
好维护
好维护的信息通常是由修改可以推出查询,比如修改是将一个区间加上某个数,查询是查区间和,这时可以直接由修改推出查询。
P3373 【模板】线段树 2
比单纯的区间加稍微复杂一点。
这题显然是好维护的,对于一个区间加上一个数,很典,乘上一个数,考虑添加一个乘法懒标记。记 \(tag1\) 为加法标记,\(tag2\) 为乘法标记。
这时我们要考虑,加法标记和乘法标记的优先级。对于一个运算:
不难发现,\(1\) 乘了 \(4\times 7\),但是 \(6\) 只乘了 \(7\),这提示我们不能直接将 \(tag1\) 累加至 \(sum\),再用 \(tag2\) 去乘,此时我们将这个柿子的顺序变换一下:
这启示我们加法标记 \(tag1\) 存的实际是 \(1\times 4\times 7+6\times 7\),\(tag2\) 存的是 \(4\times 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 方差
对于平均数,这是很好维护的,只需维护区间和即可。
对于方差,我们利用高中数学知识将其化成如下形式:
对于这个玩意,维护区间平方和即可,考虑修改对查询的影响,若给\(a_{l\sim r}+k\),那么区间平方和为 \((a_{l}+k)^2+...+(a_{r}+k)^2\) 即 \(a_{l}^2+...+a_{r}^2+2k(a_l+...+a_r)+(r-l+1)\times k^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;
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」文文的摄影布置
比较有意思的线段树。
注意到要维护的式子是 \(A_i+A_k-min(B_j)(i<j<k)\) 的 \(\max\),考虑将式子拆成两个部分:\(A_i\) 和 \(A_k-min(B_j)\) 或者 \(A_k\) 和 \(A_i-min(B_j)\) ,之所以将式子拆成两个部分是因为 \(i,j\) 与 \(k,j\) 的相对顺序不一样。
维护是简单的,只需维护区间 \(A_{\max}\),区间 \(B_{\min}\),区间 \(A_i-min(B_j)\),区间 \(A_k-min(B_j)\),区间 \(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\),毕竟区间 \(+k\),\(\gcd\) 显然不是 \(+k\)。
这是就要提到 \(\gcd\) 的一个性质:
通过这个式子,我们发现,区间的 \(\gcd\) 与其差分序列的 \(\gcd\) 是相等的,所以我们考虑直接维护差分序列,这样对于区间加操作,转换为修改两个点的权值,在回溯时暴力更新 \(\gcd\) 即可,时间复杂度 \(O(\log n)\)。
对于 \((l,r)\) 的询问,也就是 \((a[l],(b[l+1]...b[r]))\),其中 \(b\) 为差分数组,只需对于 \(a\) 再开一棵线段树即可。
总时间复杂度 \(O(m\log n)\)。
不好维护
这类题通常不好维护,特征是询问很正常,但是修改却很奇怪,这类题目的通解便是——暴力修改,但同时修改会有性质,即一个点被修改的次数有限。
P4145 上帝造题的七分钟 2 / 花神游历各国
相当经典的题目,询问区间和,修改为开方。
对于开方,显然没有太好的处理办法,毕竟原来的区间和是 \(sum\),将区间内每一个数字开方后,区间和不是 \(\sqrt{sum}\),所以根本没法用懒标记维护。
但是我们考虑一个点最多会被开方几次,极限情况是 \(10^{12}\),被开方 \(6\) 次就到 \(1\),到 \(1\) 以后在开方值也不会变,利用这个性质,可以在每个节点记录该区间内是否全部数字都是 \(1\),如果是,就不用修改;否则一个一个暴力修改。
时间复杂度\(O(6m\log n)\)
点击查看代码
#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\),若 \(x\;\and\;k=k\),即可说明所有数字都包含 \(k\),即为无效修改。对于有效修改,暴力修改即可。
时间复杂度 \(O(30m\log n)\)。
注意,本题的子段和可以不选。
点击查看代码
#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\),若 \(x\mod y\) 且 \(y\ge\frac{x}{2}\),那么结果小于 \(\frac{x}{2}\),若 \(y\le\frac{x}{2}\),结果也会小于 \(\frac{x}{2}\)。
所以,一个数字最多模 \(\log a\) 次,维护区间是否全部为 \(1\),不是则直接暴力修改即可。
时间复杂度 \(O(m\log a\log n)\)。
点击查看代码
#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;
}