线段树进阶(势能分析,segbeats,历史值标记)
例题 1:#228. 基础数据结构练习题
考虑区间开方之后极差的变化。根据
若区间的极差在开方之后不变(极差为
若区间的极差在开方之后变化,那么递归修改两个子区间,然后这个区间的极差至少会开方。
用线段树维护,维护的信息有区间和、区间最大值、区间最小值,标记有区间加标记。修改操作只会导致涉及到的
例题 2:HDU5306 Gorgeous Sequence
题意:区间和
这种区间取
维护区间最大值
考虑让当前区间和
分析一下这样做的时间复杂度。初始时线段树上所有区间的数的种类数之和是
例题 3:#4695. 最假女选手
不考虑区间加操作,和上一题差不多,多维护最小值、次小值、最小值出现次数、取
考虑区间加操作,增加一个区间加标记,优先级高于取
实现时有一个 trick,就是不需要记录取
分析复杂度。
区间加操作最坏情况可能让一个区间内数的种类数翻倍,所以上一题的证明不管用了。
对于线段树上的结点
考虑区间取最小值操作暴力递归的过程。假设要和
只要操作满足区间内值相同的数操作后值仍然相同,那么“一次区间修改操作只会增加
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
int u,v,w,mn,mx;
ll sm;
struct T{ll s;int i,i2,ic,a,a2,ac,t,l;}s[1048579];
//s表示区间和,i表示最小值,i2表示次小值,ic表示最小值出现次数,t表示区间加标记,l表示区间长度
#define _ s[k],s[k*2],s[k*2+1]
#define I int k,int l,int r
#define M int m=(l+r)>>1;
#define L k*2,l,m
#define R k*2+1,m+1,r
#define W(F) M if(dn(_),u<=m)F(L);if(m<v)F(R);up(_);
auto ad=[](T&k,int v){k.i+=v,k.i2+=v,k.a+=v,k.a2+=v,k.t+=v,k.s+=k.l*1ll*v;};//k对应区间加v
auto cmn=[](T&k,int v){//k对应区间和v取min,保证k.a2<v<k.a
if(k.i2==k.a)k.i2=v;else if(k.i==k.a)k.i=v;
k.s+=(v-k.a)*1ll*k.ac,k.a=v;
};
auto cmx=[](T&k,int v){//取max
if(k.a2==k.i)k.a2=v;else if(k.a==k.i)k.a=v;
k.s+=(v-k.i)*1ll*k.ic,k.i=v;
};
auto dn=[](T&k,T&a,T&b){//下传三种标记
if(k.t)ad(a,k.t),ad(b,k.t),k.t=0;
if(k.a<a.a)cmn(a,k.a);if(k.a<b.a)cmn(b,k.a);
if(k.i>a.i)cmx(a,k.i);if(k.i>b.i)cmx(b,k.i);
};
auto up=[](T&k,T&a,T&b){//上传信息
if(k.s=a.s+b.s,a.a>b.a)k.a=a.a,k.ac=a.ac,k.a2=max(a.a2,b.a);
else if(a.a<b.a)k.a=b.a,k.ac=b.ac,k.a2=max(a.a,b.a2);else k.a=a.a,k.ac=a.ac+b.ac,k.a2=max(a.a2,b.a2);
if(a.i<b.i)k.i=a.i,k.ic=a.ic,k.i2=min(a.i2,b.i);
else if(a.i>b.i)k.i=b.i,k.ic=b.ic,k.i2=min(a.i,b.i2);else k.i=a.i,k.ic=a.ic+b.ic,k.i2=min(a.i2,b.i2);
};
void bd(I){//建树
if(s[k].l=r-l+1,l==r){cin>>w,s[k].s=s[k].i=s[k].a=w,s[k].a2=-(s[k].i2=1e9),s[k].ac=s[k].ic=1;return;}
M bd(L),bd(R),up(_);
}
void add(I){if(u<=l&&r<=v)return ad(s[k],w);W(add)}//区间加
void umx(I){//区间取max
if(s[k].i>=w)return;
if(u<=l&&r<=v&&s[k].i2>w)return cmx(s[k],w);W(umx)
}
void umn(I){//区间取min
if(s[k].a<=w)return;
if(u<=l&&r<=v&&s[k].a2<w)return cmn(s[k],w);W(umn)
}
void qry(I){//区间查询
if(u<=l&&r<=v){sm+=s[k].s,mn=min(mn,s[k].i),mx=max(mx,s[k].a);return;}
M if(dn(_),u<=m)qry(L);if(m<v)qry(R);
}
int main(){ios::sync_with_stdio(0),cin.tie(0);
int n,m,i;
for(cin>>n,bd(1,1,n),cin>>m;m--;)if(cin>>i>>u>>v,i==1)cin>>w,add(1,1,n);
else if(i==2)cin>>w,umx(1,1,n);else if(i==3)cin>>w,umn(1,1,n);
else mn=1e9,mx=-1e9,sm=0,qry(1,1,n),cout<<(i==4?sm:(i==5?mx:mn))<<'\n';
}
例题 4:CF855F Nagini
对正负数分别考虑,转化为区间取
假设初始值为
例题 5:CF793F Julia the snail
将询问按
考虑 pushdown 如何实现,假设父亲结点为
复杂度证明和例题 2 一样,为
例题 6:CF1290E Cartesian Tree
考虑从小到大加数,
例题 7:
P4314 CPU 监控
区间加,区间赋值,求区间最大值,求区间历史最大值的最大值。
对于原数列
考虑只有区间加。维护当前加法标记
考虑加上赋值操作。容易发现,如果存在赋值标记,那么区间的所有数都会变得相同,之后的加法操作也可以视为赋值。于是 pushdown 可以转化为先加再赋值,或者只有加没有赋值。维护当前赋值标记
pushup 很简单,
时间复杂度
#include<bits/stdc++.h>
using namespace std;
#define L k*2,l,m
#define R k*2+1,m+1,r
#define M int m=(l+r)>>1;
#define _ s[k],s[k*2],s[k*2+1]
#define I int k,int l,int r
#define W(F) M if(dn(_),u<=m)F(L);if(m<v)F(R);
int a[100009],u,v,w,w1;
struct T{int a,a1,c,c1,w,w1;bool b;}s[262199];
auto ad=[](T&k,int t,int t1){if(k.w1=max(k.w1,k.w+t1),k.w+=t,k.b)k.c1=max(k.c1,k.c+t1),k.c+=t;else k.a1=max(k.a1,k.a+t1),k.a+=t;};//下传加法标记
auto cv=[](T&k,int t,int t1){if(k.c=k.w=t,k.w1=max(k.w1,t1),k.b)k.c1=max(k.c1,t1);else k.b=1,k.c1=t1;};//下传赋值标记
auto dn=[](T&k,T&a,T&b){if(ad(a,k.a,k.a1),ad(b,k.a,k.a1),k.a=k.a1=0,k.b)cv(a,k.c,k.c1),cv(b,k.c,k.c1),k.b=0;};//pushdown
auto up=[](T&k,T&a,T&b){k.w=max(a.w,b.w),k.w1=max(a.w1,b.w1);};//pushup
void bd(I){if(l==r){s[k].w=s[k].w1=a[l];return;}M bd(L),bd(R),up(_);}//建树
void add(I){if(u<=l&&r<=v)return ad(s[k],w,w);W(add) up(_);}//区间加
void cov(I){if(u<=l&&r<=v)return cv(s[k],w,w);W(cov) up(_);}//区间赋值
void qry(I){if(u<=l&&r<=v){w=max(w,s[k].w),w1=max(w1,s[k].w1);return;}W(qry);}//区间最大值和历史最大值
int main(){ios::sync_with_stdio(0);cin.tie(0);
int n,q,i;char c;
for(cin>>n,i=1;i<=n;++i)cin>>a[i];
for(bd(1,1,n),cin>>q;q--;)if(cin>>c>>u>>v,c=='Q'||c=='A')w=w1=INT_MIN,qry(1,1,n),cout<<(c=='Q'?w:w1)<<'\n';else if(cin>>w,c=='P')add(1,1,n);else cov(1,1,n);
}
例题 8:
SP1557 GSS2 - Can you answer these queries II
如何处理相同的数只算一次的条件?考虑用类似 P1972 [SDOI2009]HH的项链 的做法。将询问离线按右端点从小到大排序,从左到右依次加入序列中的元素。设当前加入
例题 9:CF997E Good Subsegments
弱化版:CF526F Pudding Monsters 题意转化为有一个长为
再考虑一个弱化版,询问区间
考虑此题。若区间
例题 10:G. Prof. Pang's sequence
从小到大枚举右端点,记
用类似的做法也可以解决区间加,区间历史版本和,
例题 11:P6242 【模板】线段树 3
三倍经验:#169. 【UR #11】元旦老人与数列 #170. Picks loves segment tree VIII
修改:区间加,区间取
查询:区间和,区间最大值,区间历史最大值的最大值
例题 3 + 例题 7。
考虑例题 3 的做法,需要维护最大值和次大值,以及取
容易发现一个性质,就是和
现在只有加法这一种标记了,就可以沿用例题 7 的做法,增加两个历史值标记
uoj 170 还要稍微复杂一些。考虑维护三种加法标记
总结:对于一类需要支持区间取
uoj 170 代码:
#include<bits/stdc++.h>
using namespace std;
#define S s[k]
#define _ s[k*2],s[k*2+1]
#define F(o) o<mx?(o>mn?t:ti):ta
const int I=2e9+9;//inf
int n,m,o,u,v,w;
struct L{//存标记的结构体
int t,a,i;//t是加法标记,a是t的历史max,i是历史min
L(int v=0):t(v),a(v),i(v){}
void operator+=(const L&x){a=max(a,t+x.a),i=min(i,t+x.i),t+=x.t;}//标记合并
};
struct T{
L t,ta,ti;int a,a2,ah,i,i2,ih;//ta是最大值标记,ti是最小值标记,t是其他值标记,a是最大值,a2是次大值,ah是历史最大值,i是最小值,i、i2同理
void ada(const L&x){i2==a?i2+=x.t:0,ah=max(ah,a+x.a),ta+=x,a+=x.t;}//最大值加上标记x,注意次小值等于最大值的情况
void adi(const L&x){a2==i?a2+=x.t:0,ih=min(ih,i+x.i),ti+=x,i+=x.t;}//最小值加上标记x
void ad(const L&x){if(i2<=a2)i2+=x.t,a2+=x.t,t+=x;}//其他值加上标记x,如果没有其他值就什么都不做,防止inf加上x.t以后爆int
void dn(T&x,T&y){//下传标记
int mn=min(x.i,y.i),mx=max(x.a,y.a);
x.ada(F(x.a)),y.ada(F(y.a)),x.adi(F(x.i)),y.adi(F(y.i)),x.ad(t),y.ad(t),t=ta=ti=0;//下传时分三种情况讨论,等于mn,等于mx,都不等于
}
void up(T&x,T&y){//上传信息
ih=min(x.ih,y.ih),i=min(x.i,y.i),i2=min(i<x.i?x.i:x.i2,i<y.i?y.i:y.i2);
ah=max(x.ah,y.ah),a=max(x.a,y.a),a2=max(a>x.a?x.a:x.a2,a>y.a?y.a:y.a2);
}
}s[1048599];
void umx(int k){if(S.i<w)S.i2>w?(S.i<S.a?void():S.ada(w-S.i),S.adi(w-S.i)):(S.dn(_),umx(k*2),umx(k*2+1),S.up(_));}//区间取max,次小值大于w时修改区间最小值,注意区间内只有一种值的时候需要将区间最大值也修改
void umn(int k){if(S.a>w)S.a2<w?(S.i<S.a?void():S.adi(w-S.a),S.ada(w-S.a)):(S.dn(_),umn(k*2),umn(k*2+1),S.up(_));}//区间取min
void bd(int k,int l,int r){//建树
if(l==r)cin>>w,S.a=S.ah=S.i=S.ih=w,S.a2=-I,S.i2=I;
else{int m=(l+r)>>1;bd(k*2,l,m),bd(k*2+1,m+1,r),S.up(_);}
}
void wk(int k,int l,int r){//处理所有操作
if(u<=l&&r<=v)o<2?(S.ada(w),S.adi(w),S.ad(w)):(o<3?umx(k):(o==5?umn(k):(o<5?w=min(w,o<4?S.i:S.ih):w=max(w,S.ah),void())));
else{int m=(l+r)>>1;if(S.dn(_),u<=m)wk(k*2,l,m);if(m<v)wk(k*2+1,m+1,r);S.up(_);}
}
int main(){ios::sync_with_stdio(0);cin.tie(0);
for(cin>>n>>m,bd(1,1,n);m--;){
if(cin>>o>>u>>v,o<3||o==5)cin>>w;else if(o<5)w=I;else w=-I;
if(wk(1,1,n),o>2&&o!=5)cout<<w<<'\n';
}
}
例题 12:Picks loves segment tree VI
维护
考虑维护
再维护
对每套标记,维护对应的
pushup 的时候好像需要高维前缀和,处理子区间最小值不等于当前区间最小值的情况,复杂度是
例题 13:#515. 【UR #19】前进四
可以直接用线段树维护区间单调栈(楼房重建) ,复杂度
考虑此题的性质,就是右端点一定是
例题 14:#671. 【UNR #5】诡异操作
考虑没有区间与操作怎么做。显然一个数只会被除
考虑加上区间与操作。对与操作打标记。对每个结点开一个长度为
考虑优化。容易发现递归到线段树深层结点的时候,数组中的数都非常小,不超过区间长度 __int128,第
例题 15:#164. 【清华集训2015】V
直接套 seg-beats 是两个
可以将所有修改操作转化为先加
考虑对区间打标记
考虑历史值询问,再加一对历史值标记
考虑下传标记,将子区间的标记与
例题 16:P6109 [Ynoi2009] rprmq1
考虑将修改差分,变为
从小到大枚举
考虑“猫树分治”,也就是两区间合并。其实本题并没有进行区间的合并,只是用到了猫树的分治结构。将查询的