题解 P6242 【模板】线段树 3

· · 题解

相信不少初学者对于 区间取 max/min 接受尚可,而对于 区间历史最值 一脸懵逼。

如果我们不满足于认识现有的结果,就要发掘出问题背后的脉络,
认识到看起来“神机妙算”的证明,拆掉之前的脚手架长什么样。

Carl Friedrich Gauss: “凡是有自尊心的建筑师,在瑰丽的大厦建成之后,决不会把脚手架留在那里。”

(摘自 EI's blog)

本文将指出,看似神机妙算的区间历史最值,其实是个简单的线性代数问题。

首先我们要明晰线段树为什么能够实现区间信息的快速维护。

对于 【模板】线段树 1:

给定序列 am 次操作:

  • \displaystyle\sum_{i=l}^r a_i

通过线段树上每个点存储 所辖区间的 a_i 之和,即可实现快速查询。

想一想为什么这么做是对的,易发现原因是 加法满足结合律

a_l+a_{l+1}+\cdots+a_{mid}+a_{mid+1}+a_{mid+2}+\cdots+a_r =(a_l+a_{l+1}+\cdots+a_{mid})+(a_{mid+1}+a_{mid+2}+\cdots+a_r)

接下来,通过在线段树上打 懒标记 ,即可实现快速修改。

同样的,其原因是 加法满足结合律和交换律

(a_i+k)+l=a_i+(k+l) (a_l+k)+(a_{l+1}+k)+\cdots+(a_r+k)=(a_l+a_{l+1}+\cdots+a_r)+(r-l+1)k

在此基础上,对于【模板】线段树 2,分别维护加法和乘法标记,
采取 先乘后加 的推标记方法,即可实现快速修改。
其原因是 乘法满足结合律,且对加法满足分配律

(a_i\times k)\times l=a_i\times(k\times l) a_l\times k+a_{l+1}\times k+\cdots+a_r\times k=(a_l+a_{l+1}+\cdots+a_r)\times k

接下来,我们看看最基础的区间历史最值问题。

给定序列 a,b,初始 a=bm 次操作:

  • \displaystyle\max_{i=l}^ra_i
  • \displaystyle\max_{i=l}^rb_i

每次操作后,我们都进行一次更新,让 b_i\leftarrow \max(b_i,a_i)

这里,我们只要维护 +,\max 两种运算,它们满足

这时,我们发现,如果将 \max 作为矩阵元素的加法,

> 维护向量序列 $\displaystyle\left\{{a_i\brack b_i}\right\},m$ 次操作: > - $\forall i\in[l,r],\displaystyle{a_i\brack b_i}_{_{}}\leftarrow\begin{bmatrix}k&-\infty\\ -\infty&0\end{bmatrix}{a_i\brack b_i}$ 。 > - 求 $\displaystyle {a_l\brack b_l}+{a_{l+1}\brack b_{l+1}}+\cdots+{a_r\brack b_r}$,$+$ 为向量加法。 > > 每次操作后,令 $\displaystyle{a_i\brack b_i}\leftarrow\begin{bmatrix}0&-\infty\\ 0&0\end{bmatrix}{a_i\brack b_i}$ 。 根据矩阵的运算律,相信大家已经发现了维护的方法: 维护 向量之和 与 矩阵乘法标记 即可。 但 矩阵乘法标记 还可以再简化,因为 $$\begin{bmatrix}a&-\infty\\b&0\end{bmatrix}+\begin{bmatrix}c&-\infty\\d&0\end{bmatrix}=\begin{bmatrix}\max(a,c)&-\infty\\\max(b,d)&0\end{bmatrix}$$ $$\begin{bmatrix}a&-\infty\\b&0\end{bmatrix}\begin{bmatrix}c&-\infty\\d&0\end{bmatrix}=\begin{bmatrix}a+c&-\infty\\\max(b+c,d)&0\end{bmatrix}$$ 于是对于 $\begin{bmatrix}c&-\infty\\d&0\end{bmatrix}$ 只要维护 $\displaystyle{c\brack d}$ 即可。 事实上,$c,d$ 即对应论文中的 “加减标记” 和 “历史最大加减标记”。 ---------------------------- 回到本题,本题在上面的基础上还支持区间取 $\min$ 和区间求和,即 > - $\forall i\in[l,r],a_i\leftarrow \min(a_i,k)$ 。 > - 求 $\displaystyle\sum_{i=l}^r a_i$ 。 回想下不维护区间历史最值时是怎样维护区间取 min 的。实际上,我们是通过摊还分析, 划分最大值与次大值,将区间取 min 转化为 **对最大值的区间加减** 来维护的。 对于本题,沿用这一思路。 我们将 $\displaystyle{a_i\brack b_i}$ 按照 $a_i$ 的大小划分 。具体地,每个节点维护如下信息: - $\overrightarrow{mx}$:$a_i$ 最大的 $\displaystyle{a_i\brack b_i}_{}$ 之和。 - $\overrightarrow{se}$:$a_i$ 非最大的 $\displaystyle{a_i\brack b_i}$ 之和。 - 矩阵 $M$:对 $\overrightarrow{mx}$ 的矩阵乘法标记。 - 矩阵 $E$:对 $\overrightarrow{se}$ 的矩阵乘法标记。 线段树维护即可。 特别地,尽管区间求和不能用矩阵来表示, 但也是可以集成到这棵线段树上的,这里留给读者作为思考题。 --------------------------- 令我吃惊的是,在此之前并没有一篇题解是基于矩阵来理解吉老师线段树的, 显然矩阵让各个操作的组织变得容易了许多。 希望这篇题解能让初学者不被繁杂的标记劝退,少走弯路,更深入地理解这个数据结构。 ```cpp /* this code is made by warzone 2022.2.14 21:25 */ #include<stdio.h> #include<string.h> typedef long long ll; typedef unsigned int word; struct READ{//快读 char pool[5<<22],*top,w; inline READ(){fread(top=pool,5<<22,1,stdin);} template<typename type> inline READ& operator >>(type& num){ for(w=1;'0'>*top||*top>'9';) w=*(top++)=='-'? -1:1; for(num=0;'0'<=*top&&*top<='9';) num=num*10+(*(top++)-'0'); return num*=w,*this; } }cin; const int inf=0x80000000;//−∞ inline int max(const int a,const int b){ return a>b? a:b;} /* 向量/矩阵 ⎡a⎤ ⎡a −∞⎤ ⎣b⎦ ⎣b 0⎦ */ struct matrix{ int a,b; inline matrix(){} inline matrix(const matrix &p)=default; inline matrix(int A,int B):a(A),b(B){} inline matrix operator+(const matrix &p)const{//矩阵/向量 加法 return matrix(max(a,p.a),max(b,p.b));} inline matrix operator()(const matrix &p)const{//矩阵乘法 return matrix(a==inf||p.a==inf? inf:a+p.a, b==inf||p.a==inf? p.b:max(b+p.a,p.b));} inline bool operator!=(const matrix &p)const{ return a!=p.a||b!=p.b;} }const I(0,inf),vec0(inf,inf); word n,m; struct segment_tree{//线段树(zkw) ll sum[1u<<20]; int cnt[1u<<20]; matrix mx[1u<<20],M[1u<<20]; matrix se[1u<<20],E[1u<<20]; inline segment_tree(){ for(word i=0;i<(1u<<20);++i) M[i]=E[i]=I; se[1u<<19]=vec0,cnt[1u<<19]=1,cin>>n>>m; for(int i=(1u<<19)+1,num;i<=(1u<<19|n);++i){ se[i]=vec0,cnt[i]=1,cin>>num; sum[i]=mx[i].a=mx[i].b=num; } for(word i=(1u<<19|n)+1;i<(1u<<20);++i) se[i]=vec0,cnt[i]=1; for(word i=(1u<<19)-1;i;--i) pushup(i); } #define l (rt<<1) #define r (rt<<1|1) inline void pushup(const word rt){//上传信息 sum[rt]=sum[l]+sum[r]; if(mx[l].a>mx[r].a){ mx[rt]=mx[l],cnt[rt]=cnt[l]; se[rt]=se[l]+se[r]+mx[r]; }else if(mx[l].a<mx[r].a){ mx[rt]=mx[r],cnt[rt]=cnt[r]; se[rt]=se[l]+se[r]+mx[l]; }else{ mx[rt]=mx[l]+mx[r]; cnt[rt]=cnt[l]+cnt[r]; se[rt]=se[l]+se[r]; } } inline void Mmul(const matrix &p,const word id){//修改 mx mx[id]=p(mx[id]),M[id]=p(M[id]),sum[id]+=1ll*cnt[id]*p.a;} inline void Emul(const matrix &p,const word id,const word size){//修改 se se[id]=p(se[id]),E[id]=p(E[id]); sum[id]+=1ll*(size-cnt[id])*p.a; } inline void pushdown(const word rt,const word size){//下传标记 Mmul(mx[l].a==mx[rt].a-M[rt].a? M[rt]:E[rt],l); Mmul(mx[r].a==mx[rt].a-M[rt].a? M[rt]:E[rt],r); //注意此处不能用 mx[l].a 和 mx[r].a 的大小关系来判断划分,因为标记下传后 mx 会被修改 if(size!=1) Emul(E[rt],l,size),Emul(E[rt],r,size); M[rt]=E[rt]=I; } inline void plus(const word f,const word t,const int k, const word rt=1,const word size=1u<<18){//区间加 if(size==0||(f==0&&t==(size<<1)-1)) return Mmul(matrix(k,k),rt),Emul(matrix(k,k),rt,size? size<<1:1); auto lf=[&](word f,word t){plus(f,t,k,l,size>>1);}; auto rf=[&](word f,word t){plus(f,t,k,r,size>>1);}; pushdown(rt,size); if(f&size) rf(f&~size,t&~size); else if((t&size)^size) lf(f,t); else lf(f,size-1),rf(0,t&~size); pushup(rt); } inline void min(const word f,const word t,const int k, const word rt=1,const word size=1u<<18){//区间取 min if(k>=mx[rt].a) return; const auto lf=[&](word f,word t){min(f,t,k,l,size>>1);}; const auto rf=[&](word f,word t){min(f,t,k,r,size>>1);}; if(size==0||(f==0&&t==(size<<1)-1)){ if(k>se[rt].a) return Mmul(matrix(k-mx[rt].a,k-mx[rt].a),rt); return pushdown(rt,size),lf(0,size-1),rf(0,size-1),pushup(rt); } pushdown(rt,size); if(f&size) rf(f&~size,t&~size); else if((t&size)^size) lf(f,t); else lf(f,size-1),rf(0,t&~size); pushup(rt); } inline ll getsum(const word f,const word t, const word rt=1,const word size=1u<<18){//区间求和 if(size==0||(f==0&&t==(size<<1)-1)) return sum[rt]; const auto lf=[&](word f,word t)->ll{return getsum(f,t,l,size>>1);}; const auto rf=[&](word f,word t)->ll{return getsum(f,t,r,size>>1);}; pushdown(rt,size); if(f&size) return rf(f&~size,t&~size); else if((t&size)^size) return lf(f,t); return lf(f,size-1)+rf(0,t&~size); } inline int geta(const word f,const word t, const word rt=1,const word size=1u<<18){//区间最值 if(size==0||(f==0&&t==(size<<1)-1)) return mx[rt].a; const auto lf=[&](word f,word t)->ll{return geta(f,t,l,size>>1);}; const auto rf=[&](word f,word t)->ll{return geta(f,t,r,size>>1);}; pushdown(rt,size); if(f&size) return rf(f&~size,t&~size); else if((t&size)^size) return lf(f,t); return max(lf(f,size-1),rf(0,t&~size)); } inline int getb(const word f,const word t, const word rt=1,const word size=1u<<18){//区间历史最值 if(size==0||(f==0&&t==(size<<1)-1)) return max(mx[rt].b,se[rt].b); const auto lf=[&](word f,word t)->ll{return getb(f,t,l,size>>1);}; const auto rf=[&](word f,word t)->ll{return getb(f,t,r,size>>1);}; pushdown(rt,size); if(f&size) return rf(f&~size,t&~size); else if((t&size)^size) return lf(f,t); return max(lf(f,size-1),rf(0,t&~size)); } #undef l #undef r }tree; int main(){ for(int opt,l,r;m;--m) if(cin>>opt>>l>>r,opt==1) cin>>opt,tree.plus(l,r,opt); else if(opt==2) cin>>opt,tree.min(l,r,opt); else if(opt==4) printf("%d\n",tree.geta(l,r)); else if(opt==5) printf("%d\n",tree.getb(l,r)); else printf("%lld\n",tree.getsum(l,r)); return 0; } ```