线段树运用进阶

一、线段树分裂

类似于 \(FHQ-Treap\) 的方式,下给出模板题代码。

//Luogu5494
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+5,M=5e6+5;
int n,m,id,cnt,rt[N];
int sz[M],ls[M],rs[M];
void push_up(int x){
	sz[x]=sz[ls[x]]+sz[rs[x]];
}void add(int &x,int l,int r,int v,int k){
	if(!x) x=++id;
	if(l==r) return sz[x]+=k,void();
	int mid=(l+r)/2;
	if(v<=mid) add(ls[x],l,mid,v,k);
	else add(rs[x],mid+1,r,v,k);
	push_up(x);
}int kth(int x,int l,int r,int k){
	if(sz[x]<k) return -1;
	if(l==r) return l;
	int mid=(l+r)/2;
	if(sz[ls[x]]>=k) return kth(ls[x],l,mid,k);
	return kth(rs[x],mid+1,r,k-sz[ls[x]]);
}int merge(int x,int y,int l,int r){
	if(!x||!y) return x+y;
	if(l==r){
		sz[x]+=sz[y];
		return x;
	}int mid=(l+r)/2;
	ls[x]=merge(ls[x],ls[y],l,mid);
	rs[x]=merge(rs[x],rs[y],mid+1,r);
	return push_up(x),x;
}void spilt(int x,int &y,int k){
	if(!x) return;y=++id;
	if(sz[ls[x]]>=k) swap(rs[x],rs[y]);
	else spilt(rs[x],rs[y],k-sz[ls[x]]);
	if(sz[ls[x]]>k) spilt(ls[x],ls[y],k);
	push_up(x),push_up(y);
}int sum(int x,int l,int r,int L,int R){
	if(L<=l&&r<=R) return sz[x];
	int mid=(l+r)/2,re=0;
	if(L<=mid) re=sum(ls[x],l,mid,L,R);
	if(R>mid) re+=sum(rs[x],mid+1,r,L,R);
	return re;
}signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>m,cnt=1;
	for(int i=1,x;i<=n;i++)
		cin>>x,add(rt[1],1,n,i,x);
	while(m--){
		int opt;cin>>opt;
		if(!opt){
			int p,x,y,a,lx,rx;
			cin>>p>>x>>y;
			lx=sum(rt[p],1,n,1,y);
			rx=sum(rt[p],1,n,x,y);
			spilt(rt[p],rt[++cnt],lx-rx);
			spilt(rt[cnt],a,rx);
			rt[p]=merge(rt[p],a,1,n);
		}if(opt==1){
			int p,t;cin>>p>>t;
			rt[p]=merge(rt[p],rt[t],1,n);
		}if(opt==2){
			int p,x,q;cin>>p>>x>>q;
			add(rt[p],1,n,q,x);
		}if(opt==3){
			int p,x,y;cin>>p>>x>>y;
			cout<<sum(rt[p],1,n,x,y)<<"\n";
		}if(opt==4){
			int p,k;cin>>p>>k;
			cout<<kth(rt[p],1,n,k)<<"\n";
		}
	}return 0;
}

二、线段树优化建图

考虑要从一段区间向另一段区间连边。容易想到首先建立中转点,将 \(n^2\) 的边数降到 \(n\)。当然,我们也可以结合线段树。由于线段树每个区间都能分成 \(O(\log n)\) 个小区间,所以我们可以将边数降到 \(O(\log n)\)

通常会建立一棵入树和一棵出树,最后再把出树的点连回入树。

下给出相对模板题的题的代码。

//CF786B
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=8e5+5;
struct node{int y,c;};
bool operator<(node x,node y){
	return x.c>y.c;
}vector<node>g[N];
int n,m,s,a[N],dis[N];
priority_queue<node>q;
void dij(int s){
	memset(dis,0x3f,sizeof(dis));
	q.push({s,dis[s]=0});
	while(q.size()){
		int x=q.top().y;
		int w=q.top().c;q.pop();
		if(w!=dis[x]) continue;
		for(auto nd:g[x]){
			int y=nd.y,c=nd.c;
			if(dis[y]>w+c)
				q.push({y,dis[y]=w+c});
		}
	}
}void build(int x,int l,int r){
	if(l==r){
		g[x+4*n].push_back({x,0});
		return a[l]=x,void();
	}int mid=(l+r)/2;
	g[x*2].push_back({x,0});
	g[x*2+1].push_back({x,0});
	g[x+4*n].push_back({x*2+4*n,0});
	g[x+4*n].push_back({x*2+1+4*n,0});
	build(x*2,l,mid);
	build(x*2+1,mid+1,r);
}void get1(int x,int l,int r,int L,int R,int gt,int w){
	if(L<=l&&r<=R)
		return g[gt].push_back({x+4*n,w}),void();
	int mid=(l+r)/2;
	if(L<=mid) get1(x*2,l,mid,L,R,gt,w);
	if(R>mid) get1(x*2+1,mid+1,r,L,R,gt,w);
}void get2(int x,int l,int r,int L,int R,int gt,int w){
	if(L<=l&&r<=R)
		return g[x].push_back({gt,w}),void();
	int mid=(l+r)/2;
	if(L<=mid) get2(x*2,l,mid,L,R,gt,w);
	if(R>mid) get2(x*2+1,mid+1,r,L,R,gt,w);
}signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>m>>s,build(1,1,n);
	while(m--){
		int opt;cin>>opt;
		if(opt==1){
			int u,v,w;cin>>u>>v>>w;
			g[a[u]].push_back({a[v]+4*n,w});
		}if(opt==2){
			int u,l,r,w;cin>>u>>l>>r>>w;
			get1(1,1,n,l,r,a[u],w);
		}if(opt==3){
			int u,l,r,w;cin>>u>>l>>r>>w;
			get2(1,1,n,l,r,a[u]+4*n,w);
		}
	}dij(a[s]);
	for(int i=1;i<=n;i++)
		cout<<(dis[a[i]]>1e18?-1:dis[a[i]])<<" ";
	return 0;
}

三、线段树分治

主要特征是在一段时间内加边,将时间轴线段树化,然后进行区间加边一类的区间操作,之后离线,在每个节点加边,最后在叶子节点处计算答案。通常要结合可撤销并查集,通常时间复杂度为 \(O(n\log^2n)\)

//BZOJ4045+Luogu5494
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int n,m,k,fa[N],sz[N];
struct mer{
	int sz,x,y;
};stack<mer>st;
struct ed{
	int x,y;
};vector<ed>g[4*N];
inline void init(){
	for(int i=1;i<=2*n;i++)
		fa[i]=i,sz[i]=1;
}inline int find(int x){
	return fa[x]==x?x:find(fa[x]);
}inline void unite(int x,int y){
	x=find(x),y=find(y);
	if(x==y) return;
	if(sz[x]<sz[y]) swap(x,y);
	st.push({sz[x],x,y});
	fa[y]=x,sz[x]+=sz[y];
}inline void chg(int x,int l,int r,int L,int R,ed e){
	if(L>R) return;
	if(L<=l&&r<=R){
		g[x].push_back(e);
		return;
	}int mid=(l+r)/2;
	if(L<=mid) chg(x*2,l,mid,L,R,e);
	if(R>mid) chg(x*2+1,mid+1,r,L,R,e);
}inline void solve(int x,int l,int r){
	int ans=1,ltp=st.size();
	for(auto e:g[x]){
		if(find(e.x)==find(e.y)){ans=0;break;}
		unite(e.x,e.y+n),unite(e.x+n,e.y);
	}int mid=(l+r)/2;
	if(!ans) for(int i=l;i<=r;i++) cout<<"No\n";
	else if(l==r) cout<<"Yes\n";
	else solve(x*2,l,mid),solve(x*2+1,mid+1,r);
	while(st.size()>ltp){
		mer x=st.top();st.pop();
		fa[x.y]=x.y,sz[x.x]=x.sz;
	}
}int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>m>>k,init();
	while(m--){
		int x,y,l,r;
		cin>>x>>y>>l>>r;
		chg(1,1,k,l+1,r,{x,y});
	}solve(1,1,k);
	return 0;
}

四、线段树二分

考虑到某些题中,可能会有二分,而二分又是在线段树上进行的,这样的时间复杂度就是 \(O(n\log^2)\)

但是线段树本身就是一棵二叉树,所以可以直接在线段树上判断走左儿子还是右儿子。这样时间复杂度就是 \(O(n\log n)\) 了。

//PA2015 Siano
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e5+5,M=2e6+5;
int n,a[N],ra[M],rs[M],rt[M];
int m,sa[M],ss[M],la[M],lt[M];
//a类基础草高,s类增长率之和,t类基础时间 
//r类右端点,s类区间总和,l类懒标记 
void push_up(int x){
	int ln=x*2,rn=x*2+1;
	ra[x]=ra[rn],rt[x]=rt[rn];
	sa[x]=(rt[x]-rt[ln])*ss[ln];
	sa[x]+=sa[ln]+sa[rn];
}void down(int x,int y,int len){
	la[x]=ra[x]=la[y];
	lt[x]=rt[x]=lt[y];
	sa[x]=len*la[y];
}void push_down(int x,int l,int r){
	if(!lt[x]) return;
	int mid=(l+r)/2;
	down(x*2,x,mid-l+1);
	down(x*2+1,x,r-mid);
	la[x]=lt[x]=0;
}void build(int x,int l,int r){
	if(l==r){
		rs[x]=ss[x]=a[l];
		return;
	}int mid=(l+r)/2;
	build(x*2,l,mid);
	build(x*2+1,mid+1,r);
	ss[x]=ss[x*2]+ss[x*2+1];
	rs[x]=rs[x*2+1];
}int find(int x,int l,int r,int b,int d){
	if(l==r) return l;
	int mid=(l+r)/2;
	push_down(x,l,r);
	if(ra[x*2]+rs[x*2]*(d-rt[x*2])<=b)
		return find(x*2+1,mid+1,r,b,d);
	return find(x*2,l,mid,b,d);
}int sum(int x,int l,int r,int k,int b,int d){
	if(l>=k){
		int re=sa[x]+ss[x]*(d-rt[x])-b*(r-l+1);
		ra[x]=la[x]=b,rt[x]=lt[x]=d;
		return sa[x]=(r-l+1)*b,re;
	}int mid=(l+r)/2;push_down(x,l,r);
	int re=sum(x*2+1,mid+1,r,k,b,d);
	if(k<=mid) re+=sum(x*2,l,mid,k,b,d);
	return push_up(x),re;
}signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i];
	sort(a+1,a+n+1),build(1,1,n);
	while(m--){
		int d,b;cin>>d>>b;int k=find(1,1,n,b,d);
		if(ra[1]+rs[1]*(d-rt[1])<=b) cout<<"0\n";
		else cout<<sum(1,1,n,k,b,d)<<"\n";
	}return 0;
}

五、树套树(下为线段树套平衡树)

考虑到在数据结构的一个节点处维护一个数据结构,就可以实现单个数据结构无法完成的工作。通常单次询问 \(O(\log^2n)\)。下面这道模版体由于使用二分,所以时间复杂度为 \(O(n\log^3n)\)

//luogu3380 树套树
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+5;
const int inf=2147483647;
int n,m,a[N],rt[N];
namespace fhq_treap{
	#define ls(x) pl[x].ls
	#define rs(x) pl[x].rs
	#define sz(x) pl[x].sz
	#define rk(x) pl[x].rk
	#define val(x) pl[x].val
	struct fhq{
		int ls,rs,val,rk,sz;
	}pl[N];int id;
	inline int mk(int x){
		return pl[++id]={0,0,x,rand(),1},id;
	}inline void push_up(int x){
		sz(x)=sz(ls(x))+sz(rs(x))+1;
	}inline void spilt(int x,int v,int &a,int &b){
		if(!x) return a=b=0,void();
		if(val(x)<=v) a=x,spilt(rs(x),v,rs(a),b);
		else b=x,spilt(ls(x),v,a,ls(b));
		push_up(x);
	}inline int merge(int x,int y){
		if(!x||!y) return x+y;
		if(rk(x)<rk(y)){
			rs(x)=merge(rs(x),y);
			push_up(x);return x;
		}ls(y)=merge(x,ls(y));
		push_up(y);return y;
	}inline void add(int &x,int v){
		int a=0,b=0;spilt(x,v,a,b);
		x=merge(merge(a,mk(v)),b);
	}inline void del(int &x,int v){
		int a=0,b=0,c=0;
		spilt(x,v-1,a,b),spilt(b,v,b,c);
		x=merge(merge(a,merge(ls(b),rs(b))),c);
	}inline int kth(int x,int k){
		if(!x) return -1e9;
		if(k<=sz(ls(x))) return kth(ls(x),k);
		if(k==sz(ls(x))+1) return x;
		return kth(rs(x),k-sz(ls(x))-1);
	}inline int rak(int &x,int k){
		int a=0,b=0;
		spilt(x,k-1,a,b);
		int re=sz(a);
		x=merge(a,b);
		return re;
	}inline int pr(int &x,int k){
		int a=0,b=0;
		spilt(x,k-1,a,b);
		int re=-1e9;
		if(a) re=val(kth(a,sz(a)));
		x=merge(a,b);
		return re;
	}inline int sb(int &x,int k){
		int a=0,b=0;
		spilt(x,k,a,b);
		int re=-1e9;
		if(b) re=val(kth(b,1));
		x=merge(a,b);
		return re;
	}
}using namespace fhq_treap;
inline void build(int x,int l,int r){
	for(int i=l;i<=r;i++)
		add(rt[x],a[i]);
	if(l==r) return;
	int mid=(l+r)/2;
	build(x*2,l,mid);
	build(x*2+1,mid+1,r);
}inline void chg(int x,int l,int r,int v,int dl,int ad){
	del(rt[x],dl),add(rt[x],ad);
	if(l==r) return;
	int mid=(l+r)/2;
	if(v<=mid) chg(x*2,l,mid,v,dl,ad);
	else chg(x*2+1,mid+1,r,v,dl,ad);
}inline int rnk(int x,int l,int r,int L,int R,int k){
	if(L<=l&&r<=R) return rak(rt[x],k);
	int mid=(l+r)/2,re=0;
	if(R>mid) re=rnk(x*2+1,mid+1,r,L,R,k);
	if(L<=mid) re+=rnk(x*2,l,mid,L,R,k);
	return re;
}inline int pre(int x,int l,int r,int L,int R,int k){
	if(L<=l&&r<=R){
		int c=pr(rt[x],k);
		return (c==-1e9)?-inf:c;
	}int mid=(l+r)/2,re=-inf;
	if(R>mid) re=pre(x*2+1,mid+1,r,L,R,k);
	if(L<=mid) re=max(re,pre(x*2,l,mid,L,R,k));
	return re; 
}inline int sub(int x,int l,int r,int L,int R,int k){
	if(L<=l&&r<=R){
		int c=sb(rt[x],k);
		return (c==-1e9)?inf:c;
	}int mid=(l+r)/2,re=inf;
	if(R>mid) re=sub(x*2+1,mid+1,r,L,R,k);
	if(L<=mid) re=min(re,sub(x*2,l,mid,L,R,k));
	return re; 
}int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	srand(time(0)),cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i];
	build(1,1,n);
	while(m--){
		int opt,l,r,x;
		cin>>opt>>l>>r;
		if(opt==3){
			chg(1,1,n,l,a[l],r);
			a[l]=r;continue;
		}cin>>x;
		if(opt==2){
			int lf=-1e8,rg=1e8,ans;
			while(lf<=rg){
				int mid=(lf+rg)/2;
				int nw=rnk(1,1,n,l,r,mid)+1;
				if(nw<=x) lf=mid+1,ans=mid;
				else rg=mid-1;
			}cout<<ans<<"\n";
		}if(opt==1) cout<<rnk(1,1,n,l,r,x)+1<<"\n";
		if(opt==4) cout<<pre(1,1,n,l,r,x)<<"\n";
		if(opt==5) cout<<sub(1,1,n,l,r,x)<<"\n";
	}return 0;
}

六、吉司机线段树

前面的运用要么是利用线段树区间个数 \(\le \log n\) 的性质,要么是改变结点所维护的量。但是吉司机线段树就不一样,它的变化在于懒标记的灵活运用。其标准模版题由于过于 \(difficult\),所以我们先从两道相对简单的模板题入手:

1.区间历史极值

代表题目:\([BZOJ3064][luogu4314]\ CPU\) 监控

首先一定会维护当前区间最大值 \(mx_x\),区间历史最大值 \(hmx_x\)

由于区间加,所以需要懒标记 \(ad_x\),表示从上一次下放懒标记后,总共给这个区间加了多少。

关联到历史问题,就是懒标记 \(had_x\),表示从上一次下放懒标记后的 \(\max ad_x\)

同理,需要懒标记 \(cv_x\)\(hcv_x\),表示从上一次下放懒标记后给这个区间覆盖的值是多少,以及 \(\max cv_x\)

由于 \(ad_x\)\(cv_x\) 可能为 \(0\),所以需要单独的标记记录它们两个需不需要下放。

那么容易得到下放 \(ad_{fa}\) 时,对三个历史信息的影响为:

\[\begin{cases} hmx_x=\max(hmx_x,mx_x+had_{fa})\\ vs_x=0,had_x=\max(had_x,ad_x+had_{fa})\\ vs_x=1,hcv_x=\max(hcv_x,ad_x+hcv_{fa}) \end{cases}\]

其中 \(vs_x\) 表示此时区间覆盖能否下放,能下放代表该区间所有数一样,所以区间加转化为区间覆盖。这就是三式的逻辑。

更容易想到的,当下放 \(cv_{fa}\) 时,影响为:

\[\begin{cases} hmx_x=\max(hmx_x,hcv_{fa})\\ hcv_x=\max(hcv_x,hcv_{fa}) \end{cases}\]

处理完懒标记后,其他部分就和普通线段树一样了。时间复杂度 \(O(n\log n)\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5,M=4e5+5;
const int inf=-2147483647;
int n,q,hmx[M],had[M],hcv[M],vv[M];
int a[N],mx[M],ad[M],cv[M],vs[M];
void push_up(int x){
	mx[x]=max(mx[x*2],mx[x*2+1]);
	hmx[x]=max(hmx[x*2],hmx[x*2+1]);
}void dad(int x,int v,int hv){
	hmx[x]=max(hmx[x],mx[x]+hv),mx[x]+=v,vv[x]=1;
	if(vs[x]) hcv[x]=max(hcv[x],cv[x]+hv),cv[x]+=v; 
	else had[x]=max(had[x],ad[x]+hv),ad[x]+=v;
}void dcv(int x,int v,int hv){
	hmx[x]=max(hmx[x],hv);
	hcv[x]=max(hcv[x],hv);
	cv[x]=v,mx[x]=v,vs[x]=1;
}void push_down(int x){
	if(vv[x]){
		dad(x*2,ad[x],had[x]);
		dad(x*2+1,ad[x],had[x]);
	}if(vs[x]){
		dcv(x*2,cv[x],hcv[x]);
		dcv(x*2+1,cv[x],hcv[x]);
	}ad[x]=vs[x]=vv[x]=had[x]=hcv[x]=0;
}void build(int x,int l,int r){
	if(l==r){
		mx[x]=hmx[x]=a[l];
		return;
	}int mid=(l+r)/2;
	build(x*2,l,mid);
	build(x*2+1,mid+1,r);
	push_up(x);
}void add(int x,int l,int r,int L,int R,int v){
	if(L<=l&&r<=R) return dad(x,v,v);
	int mid=(l+r)/2;push_down(x);
	if(L<=mid) add(x*2,l,mid,L,R,v);
	if(R>mid) add(x*2+1,mid+1,r,L,R,v);
	push_up(x);
}void cover(int x,int l,int r,int L,int R,int v){
	if(L<=l&&r<=R) return dcv(x,v,v);
	int mid=(l+r)/2;push_down(x);
	if(L<=mid) cover(x*2,l,mid,L,R,v);
	if(R>mid) cover(x*2+1,mid+1,r,L,R,v);
	push_up(x);
}int maxn(int x,int l,int r,int L,int R){
	if(L<=l&&r<=R) return mx[x];
	int mid=(l+r)/2,re=inf;push_down(x);
	if(R>mid) re=maxn(x*2+1,mid+1,r,L,R);
	if(L<=mid) re=max(re,maxn(x*2,l,mid,L,R));
	return re;
}int hmax(int x,int l,int r,int L,int R){
	if(L<=l&&r<=R) return hmx[x];
	int mid=(l+r)/2,re=inf;push_down(x);
	if(R>mid) re=hmax(x*2+1,mid+1,r,L,R);
	if(L<=mid) re=max(re,hmax(x*2,l,mid,L,R));
	return re;
}signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	cin>>q,build(1,1,n);
	while(q--){
		char opt;int x,y,z;
		cin>>opt>>x>>y;
		if(opt=='P') cin>>z,add(1,1,n,x,y,z);
		if(opt=='C') cin>>z,cover(1,1,n,x,y,z);
		if(opt=='Q') cout<<maxn(1,1,n,x,y)<<"\n";
		if(opt=='A') cout<<hmax(1,1,n,x,y)<<"\n";
	}return 0;
} 

2.\(Seg-Beats\)

这实际上可以理解为真正意义上的吉司机线段树,因为吉司机在很久很久以前提出了这类线段树并给出了时间复杂度证明。

代表题目:\([BZOJ4695]\) 最假女选手

我们考虑在进行 \(a_i\gets\min(a_i,v)\) 这种操作时,什么情况下我们要修改这个区间。

我们设 \(mx_x\) 表示区间最大值,\(nx_x\) 表示区间次大值,那么当 \(mx_x\le v\) 时,你是无处下手的;当 \(nx_x<v\le mx_x\) 时,你只能修改最大值;当 \(v\le nx_x\) 时,问题就不好说了,就得实际操作一下。

在第二种情况中,我们修改时需要最大值的个数,所以还需要维护 \(cntmx_x\)

与其相对的,对于 \(a_i\gets\max(a_i,v)\),我们也需要维护 \(mn_x,nn_x,cntmn_x\) 三个值。

当然,有一种特殊情况,即 \(mx_x=mn_x\),特判一下即可。

根据吉老师的证明,时间复杂度为 \(O(n\log^2n)\)。常数也还蛮小的。但代码是相当长的。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e6+5,M=5e5+5;
int n,mx[N],mn[N],cntx[N],cntn[N];
int m,a[M],nx[N],nn[N],sm[N],ad[N];
void push_up(int x){
	int ls=x*2,rs=x*2+1;
	mx[x]=max(mx[ls],mx[rs]);
	mn[x]=min(mn[ls],mn[rs]);
	nx[x]=max(nx[ls],nx[rs]);
	nn[x]=min(nn[ls],nn[rs]);
	cntx[x]=cntn[x]=0;
	if(mx[x]!=mx[ls])
		nx[x]=max(nx[x],mx[ls]);
	else cntx[x]+=cntx[ls];
	if(mx[x]!=mx[rs])
		nx[x]=max(nx[x],mx[rs]);
	else cntx[x]+=cntx[rs];
	if(mn[x]!=mn[ls])
		nn[x]=min(nn[x],mn[ls]);
	else cntn[x]+=cntn[ls];
	if(mn[x]!=mn[rs])
		nn[x]=min(nn[x],mn[rs]);
	else cntn[x]+=cntn[rs];
	sm[x]=sm[ls]+sm[rs];
}int dmin(int x,int v,int len){
	if(mn[x]>=v) return 1;
	if(mn[x]==mx[x]){
		mn[x]=mx[x]=v;
		return sm[x]=len*v,1;
	}if(nn[x]>v){
		nx[x]=max(nx[x],v);
		sm[x]+=cntn[x]*(v-mn[x]);
		return mn[x]=v,1;
	}return 0;
}int dmax(int x,int v,int len){
	if(mx[x]<=v) return 1;
	if(mn[x]==mx[x]){
		mn[x]=mx[x]=v;
		return sm[x]=len*v,1;
	}if(nx[x]<v){
		nn[x]=min(nn[x],v);
		sm[x]+=cntx[x]*(v-mx[x]);
		return mx[x]=v,1;
	}return 0;
}void dad(int x,int v,int len){
	mx[x]+=v,mn[x]+=v,nx[x]+=v;
	nn[x]+=v,sm[x]+=len*v,ad[x]+=v;
}void push_down(int x,int llen,int rlen){
	if(ad[x]){
		dad(x*2+1,ad[x],rlen);
		dad(x*2,ad[x],llen),ad[x]=0;
	}if(mx[x]<mx[x*2]) dmax(x*2,mx[x],llen);
	if(mn[x]>mn[x*2]) dmin(x*2,mn[x],llen);
	if(mx[x]<mx[x*2+1]) dmax(x*2+1,mx[x],rlen);
	if(mn[x]>mn[x*2+1]) dmin(x*2+1,mn[x],rlen);
}void build(int x,int l,int r){
	if(l==r){
		mx[x]=mn[x]=sm[x]=a[l];
		cntx[x]=cntn[x]=1,nn[x]=1e18;
		return nx[x]=-1e18,void();
	}int mid=(l+r)/2;
	build(x*2,l,mid);
	build(x*2+1,mid+1,r);
	push_up(x);
}void add(int x,int l,int r,int L,int R,int v){
	if(L<=l&&r<=R) return dad(x,v,r-l+1);
	int mid=(l+r)/2;push_down(x,mid-l+1,r-mid);
	if(L<=mid) add(x*2,l,mid,L,R,v);
	if(R>mid) add(x*2+1,mid+1,r,L,R,v);
	push_up(x); 
}void chgn(int x,int l,int r,int L,int R,int v){
	if(L<=l&&r<=R&&dmin(x,v,r-l+1)) return;
	int mid=(l+r)/2;push_down(x,mid-l+1,r-mid);
	if(L<=mid) chgn(x*2,l,mid,L,R,v);
	if(R>mid) chgn(x*2+1,mid+1,r,L,R,v);
	push_up(x);
}void chgx(int x,int l,int r,int L,int R,int v){
	if(L<=l&&r<=R&&dmax(x,v,r-l+1)) return;
	int mid=(l+r)/2;push_down(x,mid-l+1,r-mid);
	if(L<=mid) chgx(x*2,l,mid,L,R,v);
	if(R>mid) chgx(x*2+1,mid+1,r,L,R,v);
	push_up(x);
}int sum(int x,int l,int r,int L,int R){
	if(L<=l&&r<=R) return sm[x];
	int mid=(l+r)/2,re=0;
	push_down(x,mid-l+1,r-mid);
	if(R>mid) re=sum(x*2+1,mid+1,r,L,R);
	if(L<=mid) re+=sum(x*2,l,mid,L,R);
	return re;
}int maxn(int x,int l,int r,int L,int R){
	if(L<=l&&r<=R) return mx[x];
	int mid=(l+r)/2,re=-1e18;
	push_down(x,mid-l+1,r-mid);
	if(R>mid) re=maxn(x*2+1,mid+1,r,L,R);
	if(L<=mid) re=max(re,maxn(x*2,l,mid,L,R));
	return re;
}int minn(int x,int l,int r,int L,int R){
	if(L<=l&&r<=R) return mn[x];
	int mid=(l+r)/2,re=1e18;
	push_down(x,mid-l+1,r-mid);
	if(R>mid) re=minn(x*2+1,mid+1,r,L,R);
	if(L<=mid) re=min(re,minn(x*2,l,mid,L,R));
	return re;
}signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0),cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	cin>>m,build(1,1,n);
	while(m--){
		int opt,l,r,x;
		cin>>opt>>l>>r;
		if(opt==1) cin>>x,add(1,1,n,l,r,x);
		if(opt==2) cin>>x,chgn(1,1,n,l,r,x);
		if(opt==3) cin>>x,chgx(1,1,n,l,r,x);
		if(opt==4) cout<<sum(1,1,n,l,r)<<"\n";
		if(opt==5) cout<<maxn(1,1,n,l,r)<<"\n";
		if(opt==6) cout<<minn(1,1,n,l,r)<<"\n";
	}return 0;
}

3.\(Seg-Beats+\) 区间历史极值

代表题目:【模版】线段树 \(3\)

实际上 \(Seg-Beats\)\(a_i\gets\min(a_i,v)\) 操作可以理解为最大值的修改,那么区间加的标记就有两个:最大值和其他。

由于是历史极值,所以我们还需要两个配套的历史标记。

剩下的部分相对来说就很水到渠成,不过在 \(pushdown\) 时要注意对包含最大值区间和不包含最大值区间的区别对待。

时间复杂度可以证明是 \(O(n\log^2n)\),常数也不大。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e6+5;
int n,m,sm[N],hmx[N],had[N],hax[N];
int mx[N],nx[N],cx[N],ad[N],ax[N];
void push_up(int x){
	int ls=x*2,rs=x*2+1;
	mx[x]=max(mx[ls],mx[rs]);
	nx[x]=max(nx[ls],nx[rs]);
	hmx[x]=max(hmx[ls],hmx[rs]);
	cx[x]=0,sm[x]=sm[ls]+sm[rs];
	if(mx[x]!=mx[ls])
		nx[x]=max(nx[x],mx[ls]);
	else cx[x]+=cx[ls];
	if(mx[x]!=mx[rs])
		nx[x]=max(nx[x],mx[rs]);
	else cx[x]+=cx[rs];
}void down(int x,int a,int va,int d,int vd,int len){
	sm[x]+=(a*cx[x]+d*(len-cx[x]));
	hmx[x]=max(hmx[x],mx[x]+va),mx[x]+=a;
	hax[x]=max(hax[x],ax[x]+va),ax[x]+=a;
	had[x]=max(had[x],ad[x]+vd),ad[x]+=d;
	if(nx[x]!=-1e18) nx[x]+=d;
}void push_down(int x,int ln,int rn){
	int ls=x*2,rs=x*2+1,mxn=max(mx[ls],mx[rs]);
	if(mx[ls]==mxn) down(ls,ax[x],hax[x],ad[x],had[x],ln);
	else down(ls,ad[x],had[x],ad[x],had[x],ln);
	if(mx[rs]==mxn) down(rs,ax[x],hax[x],ad[x],had[x],rn);
	else down(rs,ad[x],had[x],ad[x],had[x],rn);
	hax[x]=had[x]=ax[x]=ad[x]=0;
}void build(int x,int l,int r){
	if(l==r){
		cx[x]=1,nx[x]=-1e18;
		cin>>mx[x],hmx[x]=mx[x];
		return sm[x]=mx[x],void();
	}int mid=(l+r)/2;
	build(x*2,l,mid);
	build(x*2+1,mid+1,r);
	push_up(x);
}void add(int x,int l,int r,int L,int R,int v){
	if(L<=l&&r<=R)
		return down(x,v,v,v,v,r-l+1);
	int mid=(l+r)/2;
	push_down(x,mid-l+1,r-mid);
	if(L<=mid) add(x*2,l,mid,L,R,v);
	if(R>mid) add(x*2+1,mid+1,r,L,R,v);
	push_up(x);
}void chg(int x,int l,int r,int L,int R,int v){
	if(mx[x]<=v) return;
	if(L<=l&&r<=R&&nx[x]<v)
		return down(x,v-mx[x],v-mx[x],0,0,r-l+1);
	int mid=(l+r)/2;
	push_down(x,mid-l+1,r-mid);
	if(L<=mid) chg(x*2,l,mid,L,R,v);
	if(R>mid) chg(x*2+1,mid+1,r,L,R,v);
	push_up(x);
}int sum(int x,int l,int r,int L,int R){
	if(L<=l&&r<=R) return sm[x];
	int mid=(l+r)/2,re=0;
	push_down(x,mid-l+1,r-mid);
	if(R>mid) re=sum(x*2+1,mid+1,r,L,R);
	if(L<=mid) re+=sum(x*2,l,mid,L,R);
	return re;
}int maxn(int x,int l,int r,int L,int R){
	if(L<=l&&r<=R) return mx[x];
	int mid=(l+r)/2,re=-1e18;
	push_down(x,mid-l+1,r-mid);
	if(R>mid) re=maxn(x*2+1,mid+1,r,L,R);
	if(L<=mid) re=max(re,maxn(x*2,l,mid,L,R));
	return re;
}int hmax(int x,int l,int r,int L,int R){
	if(L<=l&&r<=R) return hmx[x];
	int mid=(l+r)/2,re=-1e18;
	push_down(x,mid-l+1,r-mid);
	if(R>mid) re=hmax(x*2+1,mid+1,r,L,R);
	if(L<=mid) re=max(re,hmax(x*2,l,mid,L,R));
	return re;
}signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>m,build(1,1,n);
	while(m--){
		int opt,l,r,x;cin>>opt>>l>>r;
		if(opt==1) cin>>x,add(1,1,n,l,r,x);
		if(opt==2) cin>>x,chg(1,1,n,l,r,x);
		if(opt==3) cout<<sum(1,1,n,l,r)<<"\n";
		if(opt==4) cout<<maxn(1,1,n,l,r)<<"\n";
		if(opt==5) cout<<hmax(1,1,n,l,r)<<"\n";
	}return 0;
}

啊啊啊似乎快写完了。

posted @   长安一片月_22  阅读(3)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示