【学习笔记】Segment Tree Beats
基础线段树操作的复杂度证明#
单点操作#
由于线段树深度是 ,同一层只会去到一个节点,复杂度是 。
区间查询#
按照当前所在区间 与询问区间 分成三种情况:
-
与 无交,退出函数。
-
是 的子区间,更新答案并退出函数。
-
除上面两种情况以外的,向下递归。
注意到前两种情况一定来自于第三种情况,且数量不会超过第三种情况的 倍,只需要考虑第三种情况。
对线段树每层进行考虑,出现第三种情况一定是在可以遍历到的区间中左右两个,因此每一层只有 个第三种情况,故 的复杂度是 。
区间修改#
打上懒标记之后,算法流程同区间查询。
合并#
设初始线段树总节点数为 ,注意到每合并一个节点相当于减少一个节点,因此复杂度是 ,通常是 。
区间最值操作#
区间最值操作指对 , 的操作。
不含区间加减的问题#
支持区间取 ,区间求 ,区间求和。
算法流程#
对每个节点维护最大值 ,严格次大值 ,最大值个数 ,和 以及标记 。
当前区间是修改区间子区间时,与 取 时进行讨论:
-
若 ,则操作不会影响到这个节点以及其子树,退出函数。
-
若 ,则操作只会影响到最大值,,退出函数。
-
若 ,向下递归修改。
下传标记时类似类似,注意到打标记的前提条件,因此下传只有前两种情况。
点击查看代码
int t;
int n,m;
int a[maxn];
struct SegmentTree{
#define mid ((l+r)>>1)
#define lson rt<<1,l,mid
#define rson rt<<1|1,mid+1,r
int mx[maxn<<2],secmx[maxn<<2],cntmx[maxn<<2];
ll sum[maxn<<2];
int tag[maxn<<2];
inline void push_up(int rt){
if(mx[rt<<1]==mx[rt<<1|1]){
mx[rt]=mx[rt<<1],secmx[rt]=max(secmx[rt<<1],secmx[rt<<1|1]),cntmx[rt]=cntmx[rt<<1]+cntmx[rt<<1|1];
sum[rt]=sum[rt<<1]+sum[rt<<1|1];
}
else if(mx[rt<<1]>mx[rt<<1|1]){
mx[rt]=mx[rt<<1],secmx[rt]=max(secmx[rt<<1],mx[rt<<1|1]),cntmx[rt]=cntmx[rt<<1];
sum[rt]=sum[rt<<1]+sum[rt<<1|1];
}
else{
mx[rt]=mx[rt<<1|1],secmx[rt]=max(mx[rt<<1],secmx[rt<<1|1]),cntmx[rt]=cntmx[rt<<1|1];
sum[rt]=sum[rt<<1]+sum[rt<<1|1];
}
}
void build(int rt,int l,int r){
tag[rt]=-1;
if(l==r){
mx[rt]=a[l],secmx[rt]=-1,cntmx[rt]=1;
sum[rt]=a[l];
return;
}
build(lson),build(rson);
push_up(rt);
}
inline void push_down(int rt){
if(tag[rt]!=-1){
if(mx[rt<<1]>tag[rt]&&secmx[rt<<1]<tag[rt]){
sum[rt<<1]+=1ll*cntmx[rt<<1]*(tag[rt]-mx[rt<<1]),mx[rt<<1]=tag[rt];
tag[rt<<1]=tag[rt];
}
if(mx[rt<<1|1]>tag[rt]&&secmx[rt<<1|1]<tag[rt]){
sum[rt<<1|1]+=1ll*cntmx[rt<<1|1]*(tag[rt]-mx[rt<<1|1]),mx[rt<<1|1]=tag[rt];
tag[rt<<1|1]=tag[rt];
}
tag[rt]=-1;
}
}
void update_min(int rt,int l,int r,int pl,int pr,int k){
if(pl<=l&&r<=pr){
if(mx[rt]<=k) return;
else if(secmx[rt]<k){
sum[rt]+=1ll*cntmx[rt]*(k-mx[rt]),mx[rt]=k;
tag[rt]=k;
return;
}
}
push_down(rt);
if(pl<=mid) update_min(lson,pl,pr,k);
if(pr>mid) update_min(rson,pl,pr,k);
push_up(rt);
}
int query_max(int rt,int l,int r,int pl,int pr){
if(pl<=l&&r<=pr) return mx[rt];
push_down(rt);
int res=0;
if(pl<=mid) res=max(res,query_max(lson,pl,pr));
if(pr>mid) res=max(res,query_max(rson,pl,pr));
return res;
}
ll query_sum(int rt,int l,int r,int pl,int pr){
if(pl<=l&&r<=pr) return sum[rt];
push_down(rt);
ll res=0;
if(pl<=mid) res+=query_sum(lson,pl,pr);
if(pr>mid) res+=query_sum(rson,pl,pr);
return res;
}
#undef mid
#undef lson
#undef rson
}S;
int main(){
t=read();
while(t--){
n=read(),m=read();
for(int i=1;i<=n;++i) a[i]=read();
S.build(1,1,n);
for(int i=1;i<=m;++i){
int opt=read(),l=read(),r=read();
if(!opt){
int k=read();
S.update_min(1,1,n,l,r,k);
}
else if(opt==1) printf("%d\n",S.query_max(1,1,n,l,r));
else printf("%lld\n",S.query_sum(1,1,n,l,r));
}
}
return 0;
}
复杂度证明#
定义一个节点的标记为这个节点子树内的最大值,并删去和父亲标记相同的所有标记,这样初始标记有 个,且满足由上到下递减的性质,具体如下图:

另一性质是,节点的严格次大值等价于子树内(不包含本身)中标记的最大值。
定义一类标记为一次修改以及对应懒标记下传时得到的标记,定义一类标记的权值为子树内含有这类标记的节点个数,定义势能函数 为所有标记的权值总和。
初始 为 级别。
把取 操作遍历的节点为两种:常规操作也会遍历到的以及暴力向下递归的,每次操作前者个数为 。同时懒标记下传时最多影响 个区间,每次下传伴随线段树的遍历,在常规遍历中共下传 次。这样可以证明常规遍历以及伴随的下传懒标记的复杂度都是 。
考虑暴力向下递归的节点,暴力向下递归意味着子树内存在小于 的标记,向下递归等价于回收的这个不合法的标记。这样每遍历到一个节点就意味着这个子树内至少有一个标记被回收,对应这个标记在当前节点产生的权值就会减少,因此每遍历到一个节点 至少减少 ,而 不超过 级别,暴力向下递归执行的次数也不超过 级别。
总复杂度是 。
包含区间加减的问题#
区间加减对维护的 影响不大,可以按照相同方法处理。
复杂度证明略有不同,修改势能函数 的定义,定义其为每个标记所在节点的深度和。
在暴力递归时,节点的原有标记在由父亲下传后一定会被立刻撤去,所以这里的下传不会产生影响。常规操作的下传相当于增加 个节点存在标记,那么深度和 增加 。区间加减的操作同理,也是增加 个节点存在标记。
暴力递归的分析和上面类似,遍历到删去一个标记的过程均摊就是每遍历一个节点 减少 ,这样总复杂度可以分析到 。
实际跑得很快。
发现在复杂度分析过程中,并没有实际用到区间加减这操作,所以区间赋值之类不影响 的操作都是可以的。
参考资料#
-
《区间最值操作与历史最值问题》 - 吉如一
作者:SoyTony
出处:https://www.cnblogs.com/SoyTony/p/Learning_Notes_about_Segment_Tree_Beats.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效