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

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

区间历史和

例题: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 lensh,又由于转移矩阵 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] vinfinfv0infinfinf0 =[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] infinfvinf0vinfinf0 =[vmax(v,h)0]
维护区间的向量 max ⁡ \max max,懒标记的矩阵积即可。注意单位矩阵的主对角线为 0 0 0

矩阵转化为标记

注意到转移的方向为 s → h s\to h sh 0 → s 0\to s 0s 0 → h 0\to h 0h,且 s → s s\to s ss 有权。

所以我们需要维护 ( 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} ainfcb0dinfinf0 xinfzy0winfinf0 = a+xinfmax(x+c,z)max(a+y,b)0max(c+y,d,w)infinf0
那么更新信息就有:
[ 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] ainfcb0dinfinf0 =[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) ss+a,hmax(h,s+b),合并标记为 a ← a + x , b ← max ⁡ ( a + y , b ) a\gets a+x,b\gets \max(a+y,b) aa+x,bmax(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 mxv 时,不需要操作。
  • 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 vms 时,此时不能更新继续递归。

根据势能分析证明,当只有最值操作时复杂度是 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 是区间和,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 ⁡ \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);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值