线段树区间最值/区间历史最值

吉司机线段树 Segment Tree Beats

Posted by TH911 on February 5, 2026

Still writing…

2016 年国家集训队论文

Segment Tree Beats!:$2016$ WC 交流课件。

Segment Tree Beats,吉司机线段树,由吉如一在 $2016$ 年国家集训队论文中提出。

在本文中,$\leftarrow$ 表示赋值。


下文中,可能会出现区间最值操作 $\mathcal O(m\log n)$ 的复杂度(单 $\log$),但是似乎于 $2025$ 年末被证伪:若存在区间最值操作,则下界为 $\mathcal O\left(m\log^2n\right)$。

会给出原论文中的复杂度。

区间最值操作

以两道例题引入。

Gorgeous Sequence

HDU5306 Gorgeous Sequence

给定序列 $a_1,a_2,\cdots,a_n$,维护操作:

  1. 对于 $\forall i\in[l,r]$,令 $a_i\leftarrow\min(a_i,x)$。
  2. 对于 $\forall i\in[l,r]$,求 $a_i$ 最大值。
  3. 对于 $\forall i\in[l,r]$,求 $a_i$ 之和。

我们将「给定 $l,r,x$,对于 $\forall i\in[l,r]$,令 $a_i\leftarrow\min(a_i,x)$」这样的操作称之为「区间最值操作」。即将区间 $[l,r]$ 内的所有数对 $x$ 取 $\min$ 或 $\max$。

根据例题,以下仅讨论区间取 $\min$;区间取 $\max$ 是类似的,并且会在下一部分讨论。

传统线段树似乎无法很好的维护,每次操作中我们的操作对象都是那些大于 $x$ 的数,而不是整个区间。那么我们考虑单独维护区间 $[l,r]$ 内的最大值 $\textit{max}$、最大值的个数 $\textit{cnt}$、次大值 $\textit{max}_2$。(当然,显然还需要维护区间和 $\textit{sum}$)

这些信息都是很好上传维护的。

而区间 $[l,r]$ 对 $x$ 取 $\min$ 时:

  1. 若 $\textit{max}\leq x$,则取 $\min$ 后肯定不变,无需操作。

  2. 若 $\textit{max}_2<x<\textit{max}$,则取 $\min$ 后所有的 $\textit{max}$ 都变为了 $x$,且其余所有部分都不变。

    更新 $\textit{sum}$ 即令 $\textit{sum}\leftarrow\textit{sum}+\textit{cntMax}(x-\textit{max})$。

    之后令 $\textit{max}\leftarrow x$,并且打一个懒标记 $\textit{tagMax}\leftarrow x$ 即可。

  3. 否则,就暴力递归处理。

原论文中证明不带区间加操作的区间最值为 $\mathcal O(m\log n)$;然而实际上似乎为 $\mathcal O\left(m\log^2n\right)$。

参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; typedef long long ll; constexpr const int N=1000000; int n,a[N+1]; struct segTree{ struct node{ int l,r; ll sum; int max,max2,cnt,tag; }t[N<<2|1]; void up(int p){ t[p].sum=t[p<<1].sum+t[p<<1|1].sum; t[p].max=::max(t[p<<1].max,t[p<<1|1].max); if(t[p<<1].max>t[p<<1|1].max){ t[p].max=t[p<<1].max; t[p].cnt=t[p<<1].cnt; t[p].max2=::max(t[p<<1].max2,t[p<<1|1].max); }else if(t[p<<1].max==t[p<<1|1].max){ t[p].max=t[p<<1].max; t[p].cnt=t[p<<1].cnt+t[p<<1|1].cnt; t[p].max2=::max(t[p<<1].max2,t[p<<1|1].max2); }else{ t[p].max=t[p<<1|1].max; t[p].cnt=t[p<<1|1].cnt; t[p].max2=::max(t[p<<1].max,t[p<<1|1].max2); } } void build(int p,int l,int r){ t[p]={l,r}; t[p].tag=-1; t[p].max2=-2147483648; if(l==r){ t[p].sum=t[p].max=a[l]; t[p].cnt=1; return; } int mid=l+r>>1; build(p<<1,l,mid); build(p<<1|1,mid+1,r); up(p); } void down(int p){ if(t[p].tag!=-1){ int max=::max(t[p<<1].max,t[p<<1|1].max); if(max==t[p<<1].max){ t[p<<1].sum-=1ll*t[p<<1].cnt*(t[p<<1].max-t[p].tag); t[p<<1].max=t[p].tag; t[p<<1].tag=t[p].tag; } if(max==t[p<<1|1].max){ t[p<<1|1].sum-=1ll*t[p<<1|1].cnt*(t[p<<1|1].max-t[p].tag); t[p<<1|1].max=t[p].tag; t[p<<1|1].tag=t[p].tag; } t[p].tag=-1; } } void chkmin(int p,int l,int r,int x){ if(l<=t[p].l&&t[p].r<=r){ if(t[p].max<=x){ return; }else if(t[p].max2<x){ t[p].sum-=1ll*t[p].cnt*(t[p].max-x); t[p].max=x; t[p].tag=x; return; } } down(p); if(l<=t[p<<1].r){ chkmin(p<<1,l,r,x); } if(t[p<<1|1].l<=r){ chkmin(p<<1|1,l,r,x); } up(p); } int max(int p,int l,int r){ if(l<=t[p].l&&t[p].r<=r){ return t[p].max; } down(p); int ans=0; if(l<=t[p<<1].r){ ans=max(p<<1,l,r); } if(t[p<<1|1].l<=r){ ans=::max(ans,max(p<<1|1,l,r)); } return ans; } ll sum(int p,int l,int r){ if(l<=t[p].l&&t[p].r<=r){ return t[p].sum; } down(p); ll ans=0; if(l<=t[p<<1].r){ ans=sum(p<<1,l,r); } if(t[p<<1|1].l<=r){ ans+=sum(p<<1|1,l,r); } return ans; } }t; main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); int T; cin>>T; while(T--){ int m; cin>>n>>m; for(int i=1;i<=n;i++){ cin>>a[i]; } t.build(1,1,n); while(m--){ int op,x,y,z; cin>>op>>x>>y; switch(op){ case 0: cin>>z; t.chkmin(1,x,y,z); break; case 1: cout<<t.max(1,x,y)<<'\n'; break; case 2: cout<<t.sum(1,x,y)<<'\n'; } } } cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> ## 最假女选手 > [Luogu P10639 BZOJ4695 最假女选手](https://www.luogu.com.cn/problem/P10639) > > 维护序列 $a_1,a_2,a_3,\cdots,a_n$,实现: > > 1. 对于 $\forall i\in[l,r]$,令 $a_i\leftarrow a_i+x$。 > 2. 对于 $\forall i\in[l,r]$,令 $a_i\leftarrow\max(a_i,x)$。 > 3. 对于 $\forall i\in[l,r]$,令 $a_i\leftarrow\min(a_i,x)$。 > 4. 对于 $\forall i\in[l,r]$,求 $a_i$ 之和。 > 5. 对于 $\forall i\in[l,r]$,求 $a_i$ 最大值。 > 6. 对于 $\forall i\in[l,r]$,求 $a_i$ 最小值。 同理,维护 $\textit{sum},\textit{tagAdd},\textit{cntMax},\textit{max},\textit{max}_2,\textit{tagMax},\textit{cntMin},\textit{min},\textit{min}_2,\textit{tagMin}$,共 $10$ 个信息。 钦定加法标记 $\textit{tagAdd}$ 优先,则逻辑和只维护一种是一样的,只是码量会大一些。 同时,进行区间加 $x$ 的时候要将 $\textit{max},\textit{min}$ 加 $x$,如果 $\textit{max}_2,\textit{min}_2,\textit{tagMax},\textit{tagMin}$ 存在的话也要加 $x$。
数集重合

如果一个区间只有 $1\sim 2$ 种数,那么肯定会出现 $\textit{max}=\textit{min}$ 或 $\textit{max}_2=\textit{min},\textit{min}_2=\textit{max}$。

这种时候也要把它一起改掉。

同时,区间取 $\min$ 的时候要更新 $\textit{tagMax}$,区间取 $\max$ 也要更新 $\textit{tagMin}$。 同时存在区间加法、区间最值操作,其复杂度为 $\mathcal O\left(m\log^2n\right)$。(在论文中也是如此。)
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; #define int ll typedef long long ll; constexpr const int N=5e5; constexpr const ll inf=0x3f3f3f3f3f3f3f3f; int n,a[N+1]; struct segTree{ struct node{ int l,r; ll sum,tagAdd; int cntMax,cntMin; ll max,max2,tagMax; ll min,min2,tagMin; int size(){ return r-l+1; } }t[N<<2|1]; void up(int p){ t[p].sum=t[p<<1].sum+t[p<<1|1].sum; if(t[p<<1].max>t[p<<1|1].max){ t[p].max=t[p<<1].max; t[p].cntMax=t[p<<1].cntMax; t[p].max2=::max(t[p<<1].max2,t[p<<1|1].max); }else if(t[p<<1].max==t[p<<1|1].max){ t[p].max=t[p<<1].max; t[p].cntMax=t[p<<1].cntMax+t[p<<1|1].cntMax; t[p].max2=::max(t[p<<1].max2,t[p<<1|1].max2); }else{ t[p].max=t[p<<1|1].max; t[p].cntMax=t[p<<1|1].cntMax; t[p].max2=::max(t[p<<1].max,t[p<<1|1].max2); } if(t[p<<1].min<t[p<<1|1].min){ t[p].min=t[p<<1].min; t[p].cntMin=t[p<<1].cntMin; t[p].min2=::min(t[p<<1].min2,t[p<<1|1].min); }else if(t[p<<1].min==t[p<<1|1].min){ t[p].min=t[p<<1].min; t[p].cntMin=t[p<<1].cntMin+t[p<<1|1].cntMin; t[p].min2=::min(t[p<<1].min2,t[p<<1|1].min2); }else{ t[p].min=t[p<<1|1].min; t[p].cntMin=t[p<<1|1].cntMin; t[p].min2=::min(t[p<<1].min,t[p<<1|1].min2); } } void build(int p,int l,int r){ t[p]={l,r}; t[p].tagMax=-inf; t[p].tagMin=inf; if(l==r){ t[p].sum=t[p].max=t[p].min=a[l]; t[p].max2=-inf,t[p].min2=inf; t[p].cntMax=t[p].cntMin=1; return; } int mid=l+r>>1; build(p<<1,l,mid); build(p<<1|1,mid+1,r); up(p); } void downAdd(int p,ll x){ t[p].sum+=t[p].size()*x; t[p].tagAdd+=x; t[p].max+=x; if(t[p].max2!=-inf){ t[p].max2+=x; } if(t[p].tagMax!=-inf){ t[p].tagMax+=x; } t[p].min+=x; if(t[p].min2!=inf){ t[p].min2+=x; } if(t[p].tagMin!=inf){ t[p].tagMin+=x; } } void downMax(int p,ll x){ if(t[p].min>x){ return; } t[p].sum+=t[p].cntMin*(x-t[p].min); t[p].tagMax=x; if(t[p].max2==t[p].min){ t[p].max2=x; } if(t[p].max==t[p].min){ t[p].max=x; } t[p].min=x; if(t[p].tagMin<x){ t[p].tagMin=x; } } void downMin(int p,ll x){ if(t[p].max<=x){ return; } t[p].sum+=t[p].cntMax*(x-t[p].max); t[p].tagMin=x; if(t[p].min2==t[p].max){ t[p].min2=x; } if(t[p].min==t[p].max){ t[p].min=x; } t[p].max=x; if(t[p].tagMax>x){ t[p].tagMax=x; } } void down(int p){ if(t[p].tagAdd){ downAdd(p<<1,t[p].tagAdd); downAdd(p<<1|1,t[p].tagAdd); t[p].tagAdd=0; } if(t[p].tagMax!=-inf){ downMax(p<<1,t[p].tagMax); downMax(p<<1|1,t[p].tagMax); t[p].tagMax=-inf; } if(t[p].tagMin!=inf){ downMin(p<<1,t[p].tagMin); downMin(p<<1|1,t[p].tagMin); t[p].tagMin=inf; } } void add(int p,int l,int r,int x){ if(l<=t[p].l&&t[p].r<=r){ downAdd(p,x); return; } down(p); if(l<=t[p<<1].r){ add(p<<1,l,r,x); } if(t[p<<1|1].l<=r){ add(p<<1|1,l,r,x); } up(p); } void chkmax(int p,int l,int r,int x){ if(l<=t[p].l&&t[p].r<=r){ if(x<=t[p].min){ return; }else if(x<t[p].min2){ downMax(p,x); return; } } down(p); if(l<=t[p<<1].r){ chkmax(p<<1,l,r,x); } if(t[p<<1|1].l<=r){ chkmax(p<<1|1,l,r,x); } up(p); } void chkmin(int p,int l,int r,int x){ if(l<=t[p].l&&t[p].r<=r){ if(t[p].max<=x){ return; }else if(t[p].max2<x){ downMin(p,x); return; } } down(p); if(l<=t[p<<1].r){ chkmin(p<<1,l,r,x); } if(t[p<<1|1].l<=r){ chkmin(p<<1|1,l,r,x); } up(p); } ll sum(int p,int l,int r){ if(l<=t[p].l&&t[p].r<=r){ return t[p].sum; } down(p); ll ans=0; if(l<=t[p<<1].r){ ans=sum(p<<1,l,r); } if(t[p<<1|1].l<=r){ ans+=sum(p<<1|1,l,r); } return ans; } ll max(int p,int l,int r){ if(l<=t[p].l&&t[p].r<=r){ return t[p].max; } down(p); ll ans=-inf; if(l<=t[p<<1].r){ ans=max(p<<1,l,r); } if(t[p<<1|1].l<=r){ ans=::max(ans,max(p<<1|1,l,r)); } return ans; } ll min(int p,int l,int r){ if(l<=t[p].l&&t[p].r<=r){ return t[p].min; } down(p); ll ans=inf; if(l<=t[p<<1].r){ ans=min(p<<1,l,r); } if(t[p<<1|1].l<=r){ ans=::min(ans,min(p<<1|1,l,r)); } return ans; } }t; main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); cin>>n; for(int i=1;i<=n;i++){ cin>>a[i]; } t.build(1,1,n); int m; cin>>m; while(m--){ int op,l,r,x; cin>>op>>l>>r; ll pl; switch(op){ case 1: cin>>x; t.add(1,l,r,x); break; case 2: cin>>x; t.chkmax(1,l,r,x); break; case 3: cin>>x; t.chkmin(1,l,r,x); break; case 4: cout<<t.sum(1,l,r)<<'\n'; break; case 5: cout<<t.max(1,l,r)<<'\n'; break; case 6: cout<<t.min(1,l,r)<<'\n'; break; } } cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> ## 区间最值转化为区间加减 本质上,我们的做法都是将区间取 $\min$ 转化为了对区间最大值的加减,将区间内的数划分为了最值、非最值来分别维护。 # 区间历史最值 ## 相关定义 * 历史最大值:$a_i$ 出现过的最大值。 * 历史最小值:$a_i$ 出现过的最小值。 * 历史版本和:$a_i$ 所有版本的和。 ## 可以用懒标记处理的问题 > [Luogu P4314 CPU 监控](https://www.luogu.com.cn/problem/P4314) > > 给定序列 $a_1,a_2,\cdots,a_n$,设 $b_1,b_2,\cdots,b_n$ 为其历史最大值,维护操作: > > 1. 对于 $\forall i\in[l,r]$,求 $a_i$ 最大值。 > 2. 对于 $\forall i\in[l,r]$,求 $b_i$ 最大值。 > 3. 对于 $\forall i\in[l,r]$,令 $a_i\leftarrow a_i+x$。 > 4. 对于 $\forall i\in[l,r]$,令 $a_i\leftarrow x$。 先不考虑赋值操作。 那么维护区间最大值 $\textit{max}$,历史最大值 $\textit{hmax}$,加法标记 $\textit{add}$ 是容易想到的。 维护 $\textit{max}$ 时简单的,直接加上 $\textit{add}$ 即可;考虑如何求解 $\textit{hmax}$。 线段树懒标记的特性决定了**懒标记会在一段时间(生存周期)内「停留」在一个位置不变**,同时子树内所有信息不变。那么我们记录生存周期内当前区间最大的 $\textit{add}$ 为 $\textit{hadd}$,则在下传更新时有: $$ \textit{hmax}=\max(\textit{hmax},\textit{max}+\textit{hadd}) $$ 维护 $\textit{hadd}$ 也不难,$\textit{hadd}\leftarrow\max(\textit{hadd},\textit{add}+hx)$ 即可。$\textit{hx}$ 表示增加 $x$ 时,上一层在一个生存周期内增加的最大值。 对于区间覆盖,这其实很简单。因为覆盖之后值都是一样的,反而很好维护。 时间复杂度 $\mathcal O(m\log n)$。(没有区间最值操作,$\mathcal O(m\log n)$ 是正确的。)
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; typedef long long ll; constexpr const int N=1e5,inf=2147483647; int n,a[N+1]; struct segTree{ struct node{ int l,r; int max,hmax; int add,hadd; int set,hset; }t[N<<2|1]; void up(int p){ t[p].max=::max(t[p<<1].max,t[p<<1|1].max); t[p].hmax=::max(t[p<<1].hmax,t[p<<1|1].hmax); } void build(int p,int l,int r){ t[p]={l,r}; t[p].set=t[p].hset=-inf; if(l==r){ t[p].max=t[p].hmax=a[l]; return; } int mid=l+r>>1; build(p<<1,l,mid); build(p<<1|1,mid+1,r); up(p); } void downAdd(int p,int x,int hx){ t[p].hmax=::max(t[p].hmax,t[p].max+hx); t[p].max+=x; if(t[p].set==-inf){ t[p].hadd=::max(t[p].hadd,t[p].add+hx); t[p].add+=x; }else{ t[p].hset=::max(t[p].hset,t[p].set+hx); t[p].set+=x; } } void downSet(int p,int x,int hx){ t[p].hmax=::max(t[p].hmax,hx); t[p].max=x; t[p].hset=::max(t[p].hset,hx); t[p].set=x; } void down(int p){ if(t[p].add||t[p].hadd){ downAdd(p<<1,t[p].add,t[p].hadd); downAdd(p<<1|1,t[p].add,t[p].hadd); t[p].add=t[p].hadd=0; } if(t[p].set!=-inf||t[p].hset!=-inf){ downSet(p<<1,t[p].set,t[p].hset); downSet(p<<1|1,t[p].set,t[p].hset); t[p].set=t[p].hset=-inf; } } int max(int p,int l,int r){ if(l<=t[p].l&&t[p].r<=r){ return t[p].max; } int ans=-inf; down(p); if(l<=t[p<<1].r){ ans=max(p<<1,l,r); } if(t[p<<1|1].l<=r){ ans=::max(ans,max(p<<1|1,l,r)); } return ans; } int hmax(int p,int l,int r){ if(l<=t[p].l&&t[p].r<=r){ return t[p].hmax; } int ans=-inf; down(p); if(l<=t[p<<1].r){ ans=hmax(p<<1,l,r); } if(t[p<<1|1].l<=r){ ans=::max(ans,hmax(p<<1|1,l,r)); } return ans; } void add(int p,int l,int r,int x){ if(l<=t[p].l&&t[p].r<=r){ downAdd(p,x,::max(x,0)); return; } down(p); if(l<=t[p<<1].r){ add(p<<1,l,r,x); } if(t[p<<1|1].l<=r){ add(p<<1|1,l,r,x); } up(p); } void set(int p,int l,int r,int x){ if(l<=t[p].l&&t[p].r<=r){ downSet(p,x,x); return; } down(p); if(l<=t[p<<1].r){ set(p<<1,l,r,x); } if(t[p<<1|1].l<=r){ set(p<<1|1,l,r,x); } up(p); } }t; int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); cin>>n; for(int i=1;i<=n;i++){ cin>>a[i]; } t.build(1,1,n); int m; cin>>m; while(m--){ char op; int l,r,x; cin>>op>>l>>r; switch(op){ case 'Q': cout<<t.max(1,l,r)<<'\n'; break; case 'A': cout<<t.hmax(1,l,r)<<'\n'; break; case 'P': cin>>x; t.add(1,l,r,x); break; case 'C': cin>>x; t.set(1,l,r,x); break; } } cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> ## 无法用懒标记处理的单点问题 此处「单点问题」除查询单点历史最值外,也包括查询区间历史最小值的最小值、查询区间历史最大值的最大值。因为这两个区间询问的处理方法与单点询问完全相同,所以把它们归入单点问题之中。 > [UOJ169 元旦老人与数列](https://uoj.ac/problem/169) > > 设序列 $a_1,a_2,\cdots,a_n$,$b_1,b_2,\cdots,b_n$ 为其历史最小值;维护操作: > > 1. 对于 $\forall i\in[l,r]$,令 $a_i\leftarrow a_i+x$。 > 2. 对于 $\forall i\in[l,r]$,令 $a_i\leftarrow\max(a_i,x)$。 > 3. 对于 $\forall i\in[l,r]$,求 $a_i$ 最小值。 > 4. 对于 $\forall i\in[l,r]$,求 $b_i$ 最小值。 无法运用懒标记维护,因为取 $\max$ 与历史最小值方向相反,信息无法合并。 但是可以直接运用区间最值操作,将其转化为区间加减后可以类似于上文维护历史最小值的最小值。 时间复杂度 $\mathcal O\left(m\log^2n\right)$。 ## 无区间最值操作的区间问题 ### 历史最小值之和 对于序列 $a_1,a_2,\cdots,a_n$,设其历史最小值为 $b_1,b_2,\cdots,b_n$;定义 $c_i=a_i-b_i$。 令 $a_i\leftarrow a_i+x$ 时: * 若 $b_i$ 不变,则令 $c_i\leftarrow c_i+x$。 * 若 $b_i$ 变为了 $a_i$,则令 $c_i\leftarrow0$。 这也就等价于让 $c_i$ 先加 $x$,再与 $0$ 取 $\max$,即 $c_i\leftarrow\max(c_i+x,0)$。 这样就可以 $\mathcal O\left(m\log^2n\right)$ 维护 $a,c$。查询 $b$ 时,二者相减即可得到。 ### 历史最大值之和 同理,对于序列 $a_1,a_2,\cdots,a_n$,设其历史最小值为 $b_1,b_2,\cdots,b_n$;定义 $c_i=a_i-b_i$。 令 $a_i\leftarrow a_i+x$,则 $c_i\leftarrow\min(c_i+x,0)$。 时间复杂度 $\mathcal O\left(m\log^2n\right)$。 ### 历史版本和之和 设序列 $a_1,a_2,\cdots,a_n$ 的历史版本和为 $b_1,b_2,\cdots,b_n$,当前版本号为 $t$。 定义 $c_i=b_i-t\cdot a_i$。 此时,$a_i\leftarrow a_i+x$ 即令 $c_i\leftarrow c_i-t\cdot x$。 因此可以 $\mathcal O(m\log n)$ 维护 $c$。 ## 有区间最值操作的区间问题 先将区间最值操作转化为区间加减,再通过上一节的方法写即可。