线段树:区间历史和 & 区间历史最值 & 区间最值操作
线段树:区间历史和 & 区间历史最值 & 区间最值操作
区间历史和
例题:Loj#193.线段树历史和。
一个数列,需要支持区间加、区间求和、区间求历史和。
矩阵乘法
每个点存 \(len,s,h\) 分别表示区间长度、区间和、区间历史和。用一个行向量表示这些信息。
区间加 \(v\) 则有转移,右相乘一个矩阵:
求 \(t\) 次历史和:
那么维护区间向量和、懒标记维护转移矩阵的积即可。
矩阵转化为标记
由于转移的方向为 \(len\to s\to h\),又由于转移矩阵 \(a_{i,j}\) 表示 \(i\) 对 \(j\) 的贡献,所以可知矩阵的对角线为 \(1\),上三角有值。
所以设转移矩阵上三角的值,计算相乘后的值。
所以可以用三个数表示一个标记,标记 \((a,b,c)\) 与标记 \((x,y,z)\) 合并后得 \((x+a,y+az+b,z+c)\)。
而对于更新信息则有:
考虑实际意义:\(a\) 是区间加的标记,\(b\) 像是加标记的历史和,\(c\) 则是历史和的次数。
通过上面的过程可以看出,矩阵乘法和标记转移本质是相同的,不过直接矩阵乘法有很多无用的乘法,常数较大,可以通过取出那些有用的值转化为标记。
通过记录。
区间历史最值
例题:CPU 监控。
一个数列,支持区间加、区间赋值、区间最值、区间历史最值。
矩阵乘法
考虑设 \(s,h\) 表示区间的最值与历史最值。
运用广义矩阵乘法:\(c_{i,j}=\max_{k} \{a_{i,k}+b_{k,j}\}\)。即把 \(\times\to+\) 且 \(+\to \max\)。向量加也要变成向量 \(\max\)。
把信息写成行向量,右乘转移矩阵。我们需要保留一项 \(0\) 用于做赋值操作。
区间加 \(v\) 并取历史最值:
区间赋值 \(v\):
维护区间的向量 \(\max\),懒标记的矩阵积即可。注意单位矩阵的主对角线为 \(0\)。
矩阵转化为标记
注意到转移的方向为 \(s\to h\) 和 \(0\to s\) 和 \(0\to h\),且 \(s\to s\) 有权。
所以我们需要维护 \((1,1)(1,2)(3,1)(3,2)\) 这四个位置的值。
那么更新信息就有:
通过记录。注意当若干 \(-\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\) 表示加标记、加标记的历史最大值。
更新信息为 \(s\gets s+a,h\gets \max(h,s+b)\),合并标记为 \(a\gets a+x,b\gets \max(a+y,b)\)。
加入最值操作
线段树显然不能直接区间取 \(\min\),考虑到区间对 \(v\) 取 \(\min\) 就是把所有大于 \(v\) 的数赋值为 \(v\),或者说把所有大于 \(v\) 的数分别减去一个数,使得它们都变成 \(v\)。
我们对一个区间取 \(\min\) 时,可以递归到每个子区间都只有一种大于 \(v\) 的值时打上标记。
具体来说我们维护三个值:区间最大值 \(mx\),区间严格次大值 \(ms\),区间最大值的个数 \(cnt\)。有这样的策略:
- 当 \(mx\le v\) 时,不需要操作。
- 当 \(ms<v<mx\),更新信息,打上标记并返回,发现此时 \(mx\) 变为 \(v\),\(cnt\) 不变。
- 当 \(v\le ms\) 时,此时不能更新继续递归。
根据势能分析证明,当只有最值操作时复杂度是 \(O(m\log n)\),而如果有区间加操作,复杂度是 \(O(m\log ^2n)\),具体证明请另找资料。
我们需要对最大值和非最大值分别维护上述两种标记,即加标记与加标记的历史最大值。
可以想到,下传标记时需要看子区间最大值是否是当前区间的最大值,然后选择下传最大值的标记或非最大值的标记。
但是这里有坑点:选择下传最大值或非最大值的标记时,不能直接用两个区间的最大值比较,因为可能其中一个下传了标记,一个没有下传标记,因此我们可以在合并时记录 \(from_x\) 表示节点 \(x\) 此前的最大值由哪一个区间而来。
时间复杂度 \(O(m\log ^2n)\)。通过记录。
具体实现
可以将懒标记封装起来,用重载运算符定义合并。
struct tag{
int a,b;
tag operator+(tag x) {
return (tag){a+x.a,max(a+x.b,b)};
}
};
一些定义。h 是历史最值,s 是区间和,t1 和 t2 是最大值和非最大值的标记。
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);
}

浙公网安备 33010602011771号