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