动态区间子段和(KTT)

顾名思义,动态区间子段和是维护区间子段和的算法,其复杂度一般是 \(n\log^3 n\) 级别的,但是基本上卡不满,基本上可以认为是 \(n \log^2 n\) 的。(毕竟 EI 本人都不太卡的满)

P5693 EI 的第六分块

模板题。

操作是区间加和求区间最大子段和。这两个操作单独分开做的话就是简单的。区间加不必多说,区间最大子段和就是维护一个前缀最大、后缀最大和总体最大直接合并即可。现在看看两个东西合起来怎么做。

我们将正常最大子段和的式子写一写看能不能找一些性质。

\[\begin{aligned} lm_u&=\max(lm_{ls},sum_{ls}+rm_{ls})\\ rm_u&=\max(rm_{rs},sum_{rs}+lm_{rs})\\ tm_u&=\max(tm_{ls},tm_{rs},rm_{ls}+lm_{rs})\\ \end{aligned} \]

其中 \(lm\) 为前缀最大子段和,\(rm\) 为后缀最大子段和,\(tm\) 为整体的区间最大子段和。还是比较直观的。
这个时候我们假设对于 \(u\) 这个节点表示的区间全部加上一个比较小的值 \(w\),使得对于 \(u\) 子树中以及 \(u\) 本身的所有 \(lm,rm,tm\)\(\max\) 的位置没有改变。什么意思呢?就是比如说原来 \(lm_{ls}\)\(sum_{ls}+rm_{ls}\) 大,于是 \(lm_u\) 就取的 \(lm_{ls}\)。现在即使整个 \(u\) 的区间都加了一个较小的 \(w\),但是这个时候 \(lm_u\) 的值仍然比 \(sum_{ls}+rm_{ls}\) 大,\(lm_u\) 仍然是取的 \(lm_{ls}\)
如果这样的情况出现,那么对于 \(u\) 而言这三个变量的维护就是方便的了。只需要知道 \(lm_{ls}\) 等最大前缀的长度,就可以 \(O(1)\) 处理出 \(u\) 对应的三个变量的大小。
那如果改变了呢?EI 的分析告诉我们,我们只需要向左右儿子递归,直到有这么一些点满足上面的条件即可。这样看着非常的暴力,但是其均摊下来复杂度的上界是 \((n+q)\log^3 n\) 的,其中 \(q\) 为询问的次数。同时很难构造出数据卡满这个上界,一般都可以视为 \((n+q)\log n\) 的复杂度。

那我们如何知道到底这个加的 \(w\) 是否足够的小呢?我们考虑对于每一个点都维护一个阈值 \(jd\)。如果 \(w>jd\),就代表这个值过大了,想做有儿子递归,否则就如上直接 \(O(1)\) 修改 即可。

考虑这个 \(jd\) 如何维护。显然有 \(jd\) 先要向左右儿子的 \(jd\)\(\min\),因为左右儿子的变了自身的值也需要重新计算。然后考虑自己的这三个变量何时可能出现取 \(\max\) 的位置有改变。

我们类比区间加,维护一个加法标记 \(tag_u\),于是对于一个区间的子串,其真实值就是

\[\text{这个子串的长度}\times tag_u+加之前的值 \]

发现这个东西显然就是一个一次函数的形式。于是我们对于上面式子里的每个 \(\max\) 里的值,都去记录一下其区间的长度与加之前的值,算一下两两之间交点的最小值,这个东西就是上文所说的 \(jd\)

然后修改和查询就是正常做,复杂度 \(O(n+q)\log^3 n\),看起来极限实际上随便过。

code

细节较多。

实际上实现的时候是开了一个结构体来维护的,看起来和普通的线段树的形式不太一样,需要注意一些细节,学习一些相对优秀的写法,比如说求两个直线的交点之类的。当然还需要注意这个交点的大小是向下取整的,因为 \(w>jd\) 的时候才向左右子树递归。
然后标记下方之后需要更新一次函数的截距也就是所谓“加之前的值”,因为标记下方之后就变成加之后的值了。在本质上就是 \(y\) 轴向右平移了一段距离,所以 \(jd\) 也要修改。
然后变量名就直接将子串长度称为 \(k\),加之前的值设为 \(b\)
多看代码理解一下即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define ld double
const int N=4e5+7,inf=2e18+7;
int n,a[N],Q;
namespace KTT{
	#define ls (u<<1)
	#define rs (u<<1|1)
	#define mid ((l+r)>>1)
	struct line{int k,b;};
	line operator + (line a,line b){return (line){a.k+b.k,a.b+b.b};}
	line operator * (line a,int b){return (line){a.k,a.k*b+a.b};}
	struct node{line l,r,t,sum;int jd;}tr[N<<2];
	int tag[N<<2];
	line cmp(line a,line b,int &x){
		if(a.k<b.k||(a.k==b.k&&a.b<b.b))swap(a,b);
		if(a.b>=b.b)return a;
		x=min(x,(a.b-b.b)/(b.k-a.k));
		return b;
	}
	node operator + (node x,node y){
		node z;z.jd=min(x.jd,y.jd);
		z.t=cmp(cmp(x.t,y.t,z.jd),x.r+y.l,z.jd);
		z.l=cmp(x.l,x.sum+y.l,z.jd);
		z.r=cmp(y.r,y.sum+x.r,z.jd);
		z.sum=x.sum+y.sum;
		return z;
	}
	void push_up(int u){tr[u]=tr[ls]+tr[rs];}
	void update(int u,int w){
		if(w>tr[u].jd)update(ls,tag[u]+w),update(rs,tag[u]+w),tag[u]=0,push_up(u);
		else tag[u]+=w,tr[u].jd-=w,tr[u].l=tr[u].l*w,tr[u].r=tr[u].r*w,tr[u].sum=tr[u].sum*w,tr[u].t=tr[u].t*w;
	}
	void push_down(int u){if(tag[u])update(ls,tag[u]),update(rs,tag[u]),tag[u]=0;}
	void build(int u,int l,int r){
		if(l==r){line tmp={1,a[l]};tr[u]={tmp,tmp,tmp,tmp,inf};return;}
		build(ls,l,mid),build(rs,mid+1,r);push_up(u);
	}
	void modify(int u,int l,int r,int ql,int qr,int w){
		if(ql<=l&&r<=qr){update(u,w);return;}
		push_down(u);if(ql<=mid)modify(ls,l,mid,ql,qr,w);if(qr>mid)modify(rs,mid+1,r,ql,qr,w);
		push_up(u);
	}
	node query(int u,int l,int r,int ql,int qr){
		if(ql<=l&&r<=qr)return tr[u];
		push_down(u);
		if(qr<=mid)return query(ls,l,mid,ql,qr);if(ql>mid)return query(rs,mid+1,r,ql,qr);
		return query(ls,l,mid,ql,qr)+query(rs,mid+1,r,ql,qr);
	}
}
using namespace KTT;
signed main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>Q;for(int i=1;i<=n;i++)cin>>a[i];
	build(1,1,n);
	while(Q--){
		int op,l,r,x;cin>>op>>l>>r;
		if(op==1)cin>>x,modify(1,1,n,l,r,x);
		else cout<<max(0ll,query(1,1,n,l,r).t.b)<<'\n';
	}
	return 0;
}

P6792 [SNOI2020] 区间和

吉司机线段树套 KTT。

还是发现两个操作分开都会做,于是两个操作合在一起就考虑把两个数据结构拼在一起。

发现对于吉司机线段树而言,线段树的节点上其可能修改的值有且仅有最小值,因此我们上文的斜率也就是“这个子串的长度”变成了字串内最小值的个数,所谓的区间加标记也变成了单独针对最大值的标记。

然后就发现其余操作是类似的,直接将两者的操作结合一下即可。唯一需要注意的点是在 push_up 的时候,由于我们要维护最大值和次大值,因此不是最大值的那一边的所有信息的斜率都要变成 0,因为我们上面改了一下定义,不是最大值的就对斜率没有贡献也就是最大值的长度在这一边为 0。

听人说如果不是区间加的话复杂度是 \((n+q)\log^2 n\) 的,不知道。

code

自己实现的时候写的很快,然后就调了一下午。注意到 update 函数中我一开始在第一行的判断中没有加入 ||w<=mf[u] 而只有前半段。这样就会导致即使 \(tag_u\) 是一开始的初始值 \(-inf\) 但是不满足后半段的条件,其也会进入函数,然后就调了一下午。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=2e5+7,inf=1e18+7;
int n,Q,a[N];
namespace KTT{
	#define ls (u<<1)
	#define rs (u<<1|1)
	#define mid ((l+r)>>1)
	struct line{int k,b;};
	line operator + (line a,line b){return (line){a.k+b.k,a.b+b.b};}
	line operator * (line a,int b){return (line){a.k,a.k*b+a.b};}
	struct node{line l,r,t,sum;int jd;}tr[N<<2];
	node init(int u){node tmp=tr[u];tmp.l.k=tmp.r.k=tmp.t.k=tmp.sum.k=0,tmp.jd=inf;return tmp;}
	int tag[N<<2],mf[N<<2],ms[N<<2];
	line cmp(line a,line b,int &x){
		if(a.k<b.k||(a.k==b.k&&a.b<b.b))swap(a,b);
		if(a.b>=b.b)return a;
		x=min(x,(a.b-b.b)/(b.k-a.k));
		return b;
	}
	node operator + (node x,node y){
		node z;z.jd=min(x.jd,y.jd);
		z.t=cmp(cmp(x.t,y.t,z.jd),x.r+y.l,z.jd);
		z.l=cmp(x.l,x.sum+y.l,z.jd);
		z.r=cmp(y.r,y.sum+x.r,z.jd);
		z.sum=x.sum+y.sum;
		return z;
	}
	void push_up(int u){
		mf[u]=min(mf[ls],mf[rs]);
		if(mf[ls]==mf[rs])ms[u]=min(ms[ls],ms[rs]),tr[u]=tr[ls]+tr[rs];
		if(mf[ls]<mf[rs])ms[u]=min(ms[ls],mf[rs]),tr[u]=tr[ls]+init(rs);
		if(mf[ls]>mf[rs])ms[u]=min(mf[ls],ms[rs]),tr[u]=init(ls)+tr[rs];
	}
	void update(int u,int w){
		if(w<=tag[u]||w<=mf[u])return;tag[u]=w,w=w-mf[u],mf[u]+=w;
		if(w>tr[u].jd)update(ls,tag[u]),update(rs,tag[u]),tag[u]=-inf,push_up(u);
		else tr[u].jd-=w,tr[u].l=tr[u].l*w,tr[u].r=tr[u].r*w,tr[u].sum=tr[u].sum*w,tr[u].t=tr[u].t*w;
	}
	void push_down(int u){if(tag[u]!=-inf)update(ls,tag[u]),update(rs,tag[u]);tag[u]=-inf;}
	void build(int u,int l,int r){
		tag[u]=-inf;
		if(l==r){line tmp={1,a[l]};tr[u]={tmp,tmp,tmp,tmp,inf},mf[u]=a[l],ms[u]=inf;return;}
		build(ls,l,mid),build(rs,mid+1,r);push_up(u);
	}
	void modify(int u,int l,int r,int ql,int qr,int w){
		if(w<=mf[u])return;
		if(ql<=l&&r<=qr&&w<ms[u]){update(u,w);return;}
		push_down(u);if(ql<=mid)modify(ls,l,mid,ql,qr,w);if(qr>mid)modify(rs,mid+1,r,ql,qr,w);
		push_up(u);
	}
	node query(int u,int l,int r,int ql,int qr){
		if(ql<=l&&r<=qr)return tr[u];
		push_down(u);
		if(qr<=mid)return query(ls,l,mid,ql,qr);if(ql>mid)return query(rs,mid+1,r,ql,qr);
		return query(ls,l,mid,ql,qr)+query(rs,mid+1,r,ql,qr);
	}
}
using namespace KTT;
signed main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>Q;for(int i=1;i<=n;i++)cin>>a[i];
	build(1,1,n);
	while(Q--){
		int op,l,r,x;cin>>op>>l>>r;
		if(op==0)cin>>x,modify(1,1,n,l,r,x);
		else cout<<max(0ll,query(1,1,n,l,r).t.b)<<'\n';
	}
	return 0;
}
posted @ 2025-07-21 20:49  all_for_god  阅读(36)  评论(0)    收藏  举报