线段树:区间历史和 & 区间历史最值 & 区间最值操作

线段树:区间历史和 & 区间历史最值 & 区间最值操作

区间历史和

例题:Loj#193.线段树历史和

一个数列,需要支持区间加、区间求和、区间求历史和。

矩阵乘法

每个点存 len,s,h 分别表示区间长度、区间和、区间历史和。用一个行向量表示这些信息。

区间加 v 则有转移,右相乘一个矩阵:

[lensh][1v0010001]=[lens+v×lenh]

t 次历史和:

[lensh][10001t001]=[lensh+s×t]

那么维护区间向量和、懒标记维护转移矩阵的积即可。

矩阵转化为标记

由于转移的方向为 lensh,又由于转移矩阵 ai,j 表示 ij 的贡献,所以可知矩阵的对角线为 1,上三角有值。

所以设转移矩阵上三角的值,计算相乘后的值。

[1ab01c001][1xy01z001]=[1x+ay+az+b01z+c001]

所以可以用三个数表示一个标记,标记 (a,b,c) 与标记 (x,y,z) 合并后得 (x+a,y+az+b,z+c)

而对于更新信息则有:

[lensh][1ab01c001]=[lens+a×lenb×len+c×s+h]

考虑实际意义:a 是区间加的标记,b 像是加标记的历史和c 则是历史和的次数。

通过上面的过程可以看出,矩阵乘法和标记转移本质是相同的,不过直接矩阵乘法有很多无用的乘法,常数较大,可以通过取出那些有用的值转化为标记。

通过记录

区间历史最值

例题:CPU 监控

一个数列,支持区间加、区间赋值、区间最值、区间历史最值。

矩阵乘法

考虑设 s,h 表示区间的最值与历史最值。

运用广义矩阵乘法:ci,j=maxk{ai,k+bk,j}。即把 ×++max。向量加也要变成向量 max

把信息写成行向量,右乘转移矩阵。我们需要保留一项 0 用于做赋值操作。

区间加 v 并取历史最值:

[sh0][vvinfinf0infinfinf0]=[s+vmax(s+v,h)0]

区间赋值 v

[sh0][infinfinfinf0infvv0]=[vmax(v,h)0]

维护区间的向量 max,懒标记的矩阵积即可。注意单位矩阵的主对角线为 0

矩阵转化为标记

注意到转移的方向为 sh0s0h,且 ss 有权。

所以我们需要维护 (1,1)(1,2)(3,1)(3,2) 这四个位置的值。

[abinfinf0infcd0][xyinfinf0infzw0]=[a+xmax(a+y,b)infinf0infmax(x+c,z)max(c+y,d,w)0]

那么更新信息就有:

[sh0][abinfinf0infcd0]=[max(s+a,c)max(s+b,h,d)0]

通过记录。注意当若干 inf 相加时可能会超过 long long 的范围,所以小于 inf 时要重新赋值为 inf,代码中使用 check 函数实现这个功能。

懒标记与信息的封装的示例代码:

const int inf=1e16;
int check(int x) {
if(x<-inf) x=-inf;
if(x>inf) x=inf;
return x;
}
struct tag{
int a,b,c,d;
tag operator+(tag x) {
return (tag){
check(a+x.a),
max(check(a+x.b),b),
max(check(c+x.a),x.c),
max(check(c+x.b),max(x.d,d))
};
}
};
struct arr {
int s,h;
arr operator+(arr x) {return (arr){max(s,x.s),max(h,x.h)};}
arr operator*(tag x) {return (arr){max(check(s+x.a),x.c),max(check(s+x.b),max(h,x.d))};}
};

区间最值操作

例题:线段树 3

修改:区间加,区间 chkmin。

查询:区间和,区间最大值,区间历史最大值。

不带赋值操作的区间历史最大值

把上一节中矩阵的第三行与第三列删去即可得到此时的转移矩阵。

s,h 表示最值、历史最值,a,b 表示加标记、加标记的历史最大值

更新信息为 ss+a,hmax(h,s+b),合并标记为 aa+x,bmax(a+y,b)

加入最值操作

线段树显然不能直接区间取 min,考虑到区间对 vmin 就是把所有大于 v 的数赋值为 v,或者说把所有大于 v 的数分别减去一个数,使得它们都变成 v

我们对一个区间取 min 时,可以递归到每个子区间都只有一种大于 v 的值时打上标记

具体来说我们维护三个值:区间最大值 mx,区间严格次大值 ms,区间最大值的个数 cnt。有这样的策略:

  • mxv 时,不需要操作。
  • ms<v<mx,更新信息,打上标记并返回,发现此时 mx 变为 vcnt 不变。
  • vms 时,此时不能更新继续递归。

根据势能分析证明,当只有最值操作时复杂度是 O(mlogn),而如果有区间加操作,复杂度是 O(mlog2n),具体证明请另找资料。

我们需要对最大值和非最大值分别维护上述两种标记,即加标记与加标记的历史最大值。 

可以想到,下传标记时需要看子区间最大值是否是当前区间的最大值,然后选择下传最大值的标记或非最大值的标记。

但是这里有坑点:选择下传最大值或非最大值的标记时,不能直接用两个区间的最大值比较,因为可能其中一个下传了标记,一个没有下传标记,因此我们可以在合并时记录 fromx 表示节点 x 此前的最大值由哪一个区间而来。

时间复杂度 O(mlog2n)通过记录

具体实现

可以将懒标记封装起来,用重载运算符定义合并。

struct tag{
int a,b;
tag operator+(tag x) {
return (tag){a+x.a,max(a+x.b,b)};
}
};

一些定义。h 是历史最值,s 是区间和,t1t2 是最大值和非最大值的标记。

int mx[N*4],ms[N*4],h[N*4],cnt[N*4],s[N*4],from[N*4];
tag t1[N*4],t2[N*4];

合并信息与下传标记。注意下传完后要把懒标记清空。这里 from[x]==0 时则说明两个子区间都是最大值,否则 from[x] 就是儿子的编号。

void pushup(int x) {
s[x]=s[ls]+s[rs];
h[x]=max(h[ls],h[rs]);
if(mx[ls]==mx[rs]) {
mx[x]=mx[ls];
cnt[x]=cnt[ls]+cnt[rs];
ms[x]=max(ms[ls],ms[rs]);
from[x]=0;
return;
}
int c1=ls,c2=rs;
if(mx[c1]<mx[c2]) swap(c1,c2);
mx[x]=mx[c1],ms[x]=max(ms[c1],mx[c2]),cnt[x]=cnt[c1];
from[x]=c1;
}
void pushdown(int x,int l,int r) {
if(l<r) {
if(ls==from[x]||!from[x]) t1[ls]=t1[ls]+t1[x];
else t1[ls]=t1[ls]+t2[x];
if(rs==from[x]||!from[x]) t1[rs]=t1[rs]+t1[x];
else t1[rs]=t1[rs]+t2[x];
t2[ls]=t2[ls]+t2[x],t2[rs]=t2[rs]+t2[x];
}
h[x]=max(h[x],mx[x]+t1[x].b);
s[x]+=cnt[x]*t1[x].a;
mx[x]+=t1[x].a;
s[x]+=(r-l+1-cnt[x])*t2[x].a;
if(ms[x]!=-inf) ms[x]+=t2[x].a;
t1[x]=t2[x]=(tag){0,0};
}

修改的写法。注意由于取 min 操作与区间最大值有关,所以要首先下传标记。

void chkmin(int x,int l,int r,int L,int R,int y) {
pushdown(x,l,r);
if(R<l||r<L) return;
if(L<=l&&r<=R) {
if(mx[x]<=y) return;
if(ms[x]<y) {
t1[x]=t1[x]+(tag){y-mx[x],y-mx[x]};
pushdown(x,l,r);
return;
}
chkmin(ls,l,mid,L,R,y),chkmin(rs,mid+1,r,L,R,y);
pushup(x);
return;
}
chkmin(ls,l,mid,L,R,y),chkmin(rs,mid+1,r,L,R,y);
pushup(x);
}
void add(int x,int l,int r,int L,int R,int y) {
if(L<=l&&r<=R) {
t1[x]=t1[x]+(tag){y,y};
t2[x]=t2[x]+(tag){y,y};
pushdown(x,l,r);
return;
}
pushdown(x,l,r);
if(R<l||r<L) return;
add(ls,l,mid,L,R,y),add(rs,mid+1,r,L,R,y);
pushup(x);
}
posted @   dengchengyu  阅读(24)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性
· 2025年我用 Compose 写了一个 Todo App
点击右上角即可分享
微信分享提示