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

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

区间历史和

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

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

矩阵乘法

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

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

\[\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} \]

\(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} \]

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

矩阵转化为标记

由于转移的方向为 \(len\to s\to h\),又由于转移矩阵 \(a_{i,j}\) 表示 \(i\)\(j\) 的贡献,所以可知矩阵的对角线为 \(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} \]

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

而对于更新信息则有:

\[\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} \]

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

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

通过记录

区间历史最值

例题:CPU 监控

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

矩阵乘法

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

运用广义矩阵乘法:\(c_{i,j}=\max_{k} \{a_{i,k}+b_{k,j}\}\)。即把 \(\times\to+\)\(+\to \max\)。向量加也要变成向量 \(\max\)

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

区间加 \(v\) 并取历史最值:

\[\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} \]

区间赋值 \(v\)

\[\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} \]

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

矩阵转化为标记

注意到转移的方向为 \(s\to h\)\(0\to s\)\(0\to h\),且 \(s\to s\) 有权。

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

\[\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} \]

那么更新信息就有:

\[\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} \]

通过记录。注意当若干 \(-\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 是区间和,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 @ 2025-02-21 22:32  dengchengyu  阅读(342)  评论(0)    收藏  举报