值域倍增分块&底层分块&题解:[Ynoi2007] rgxsxrs

题解

题目传送门

这种 >xx 的题有一种套路是值域倍增分块
就是把值域分成 [1,2),[2,4),[4,8),... 这样 O(logV) 个块。
然后我们对每一个块都开一个线段树维护序列。
对于块 [2k,2k+1) 所对应的线段树,只有那些满足 ai[2k,2k+1) 的位置 i 对线段树有贡献。
线段树维护的是区间 min,max,sum
那显然查询的时候对 O(logV) 棵线段树都问一遍就可以了,查询复杂度是 O(mlognlogV)

看修改,对于块 [2k,2k+1)

  1. x<2k:此时线段树上 [l,r] 中所有数都要 x(没有贡献的位置当然不用)。
    因为 [l,r] 会在线段树上拆成 log 个子区间,我们就对其中一个子区间 [l,r] 考虑。
    如果 [l,r] 的区间最小值 x 之后不再属于 [2k,2k+1),他会掉到下面的块中。
    因为总共只有 O(logV) 个块,所以一个位置只会掉 O(logV) 次。
    所以我们直接暴力 O(logn) 把最小值从当前线段树删去(可以用线段树二分找),加入对应块的线段树。
    这个操作的总复杂度是 O(nlognlogV)
    对于剩下的那些数直接打上区间减 tag 即可,总复杂度就是普通线段树操作的复杂度 O(mlognlogV)
    Tip: 当然不一定只有最小值要掉,次小值可能也会,次次小值可能也会,得不断删最小值直到最小值 x 之后不会掉下去。

  2. 2k+1x:啥都不用做。

  3. 2kx<2k+1:此时 [l,r] 中一些比较大的数需要 x
    但因为 x2k,所以减完之后至少减半,所以只会减半 O(logV) 次,且一定会掉到下面的块,同理暴力修改的复杂度是 O(nlognlogV)
    所以直接暴力找到所有 >x 的叶子并修改即可。

总时间复杂度是 O((n+m)lognlogV),空间复杂度是 O(nlogV)

如果就这样的话,这题还不是很毒瘤,也不是很难写。
但这是 ynoi,你会愉快地 MLE。

卡空间的方法是底层分块
其实说人话就是当线段树某个节点代表的区间长度 B 的时候,直接暴力查询/修改。
你稍微分析一下就会发现区间操作最多只会暴力 O(1) 个块。(即那 log 个子区间中最多只有头和尾这两个子区间的长度 B)。
所以此时常规线段树操作的单次复杂度变成 O(logn+B)
而对于 1,3 情况中那些暴力找最小值/最大值并修改他们的单点操作,复杂度也是 O(logn+B)
可以认为是先花 O(logn) 的时间找到他,再花 O(B) 的时间修改它。
每棵线段树的节点数就是 O(nB),空间复杂度就是 O(nlogVB)
理论上取 B=logV 可以使空间变成 O(n),时间会稍微大一点。

到这里两个 trick 就讲完了,只想学这两个 trick 的可以不用往下看了。

当然 ynoi 不卡常是不可能的。
主要就是改两个块长。
倍增值域分块的底数取 2 其实是基本没希望的,而且空间是贴着过去的(实测 59MB)。
如果我们按照 [1,K),[K,K2),[K2,K3),... 这样分:
会分成 logKV 个块,每个数只会掉 logKV 次,在情况 3 中的数至多减 K1 次就会掉到下面的块。
所以情况 1 的复杂度会变成 O(nlogKVlogn),情况 3 的复杂度会变成 O(nKlogKVlogn)
对应的如果要让空间为 O(n)B 要取 O(logKV)
虽然当 K 比较大的时候,空间不一定要 O(n)

我取了 K=32,B=40

当然,代码最终解释权归卡常所有。

code

#include<bits/stdc++.h>
#define LL long long 
#define PIIII pair<pair<LL,int>,pair<int,int>>
#define PIII pair<LL,pair<int,int>>
#define fi first
#define se second 
#define ls(p) t[p].ls
#define rs(p) t[p].rs
using namespace std;
const int N=5e5+5,K=32,B=40,V=1e9,inf=2*V,N2=240000,mod=(1<<20);

inline int read(){
	int w=1,s=0;
	char c=getchar();
	for(;c<'0'||c>'9';w*=(c=='-')?-1:1,c=getchar());
	for(;c>='0'&&c<='9';s=s*10+c-'0',c=getchar());
	return w*s;
}

int n,T,a[N],ll,rr,xx,ID[N],tmp;
int L[35],R[35],CNT,rt[35];
int tot;
int Get_id(int x){return upper_bound(L,L+CNT+1,x)-L-1;}   //返回值 x 所在值域块编号
struct node{
	int l,r,ls,rs,maxn,ming,cnt,add;  //注意 add 是不用 LL 的,不会超过 max(a[i])
	LL sum;
	void tag(int d){
		add+=d; sum+=1ll*cnt*d;    //注意不是 (r-l+1)*d
		if(cnt) ming+=d; maxn+=d;
	}
}t[N2];

void Tag(int p,int id,int l,int r,int d){  //把对应块中的所有数 +d
	LL sum=0;
	int cnt=0,maxn=0,ming=inf;
	for(int i=l;i<=r;i++){
		if(ID[i]==id){
			a[i]+=d;   
			ID[i]=Get_id(a[i]);
			cnt++,sum+=a[i],maxn=max(maxn,a[i]),ming=min(ming,a[i]);
		}
	}
	t[p].sum=sum,t[p].cnt=cnt,t[p].maxn=maxn,t[p].ming=ming;
}

void pushup(int p){
	t[p].sum=t[ls(p)].sum+t[rs(p)].sum;
	t[p].cnt=t[ls(p)].cnt+t[rs(p)].cnt;
	t[p].ming=min(t[ls(p)].ming,t[rs(p)].ming);
	t[p].maxn=max(t[ls(p)].maxn,t[rs(p)].maxn);
}
void pushdown(int id,int p){  //根据我们打懒标记的规则会发现,pushdown 完之后一定不会出现有元素掉到下一个块的情况
	if(t[p].add){
		if(t[ls(p)].r-t[ls(p)].l+1<=B) Tag(ls(p),id,t[ls(p)].l,t[ls(p)].r,t[p].add);   
		else t[ls(p)].tag(t[p].add);
		
		if(t[rs(p)].r-t[rs(p)].l+1<=B) Tag(rs(p),id,t[rs(p)].l,t[rs(p)].r,t[p].add);   
		else t[rs(p)].tag(t[p].add);
		t[p].add=0;
	}
}

void query(int p,int id,int l,int r){   //暴力查询区间的sum,min,max
	LL sum=0;
	int cnt=0,maxn=0,ming=inf;
	for(int i=l;i<=r;i++) if(ID[i]==id) cnt++,sum+=a[i],maxn=max(maxn,a[i]),ming=min(ming,a[i]);
	t[p].sum=sum,t[p].cnt=cnt,t[p].maxn=maxn,t[p].ming=ming;
}

void Insert(int id,int p,int x){   //将某个位置插入线段树
	if(t[p].r-t[p].l+1<=B){
		a[x]-=xx;   //在这个地方减掉。
		ID[x]=Get_id(a[x]);
		query(p,id,t[p].l,t[p].r);
		return;
	}
	pushdown(id,p);
	int mid=(t[p].l+t[p].r)>>1;
	if(x<=mid) Insert(id,t[p].ls,x);
	else Insert(id,t[p].rs,x);
	pushup(p);
}

void modify(int p,int id,int l,int r){  //暴力把区间 >x 的 -x 并求答案
	LL sum=0;
	int cnt=0,maxn=0,ming=inf;
	for(int i=l;i<=r;i++){
		if(ID[i]==id){
			if(ll<=i && i<=rr && a[i]>xx) a[i]-=xx,ID[i]=Get_id(a[i]);   //注意只有在操作范围内的才改
			if(ID[i]!=id){  //掉到下一个块的话要先把他加回来,不然在 insert 中会再减一次
				tmp=ID[i];
				a[i]+=xx;
				ID[i]=Get_id(a[i]);
				Insert(tmp,rt[tmp],i);	
			}  
			else cnt++,sum+=a[i],maxn=max(maxn,a[i]),ming=min(ming,a[i]);
		}
	}
	t[p].sum=sum,t[p].cnt=cnt,t[p].maxn=maxn,t[p].ming=ming;
}

void erase(int p,int id,int l,int r,int val){   //把区间 [l,r] 中一个值为 val 的数 -x,并从当前线段树删掉
	LL sum=0;
	int cnt=0,maxn=0,ming=inf;
	bool flag=false;   //只能删一个
	for(int i=l;i<=r;i++){
		if(ID[i]==id){
			if(!flag && a[i]==val){   //注意不要直接在这个地方把他 -x 不然在 insert 中 pushdown 可能会让他多减一些东西
				flag=true;
				tmp=Get_id(a[i]-xx);
				Insert(tmp,rt[tmp],i);   //掉到下一个块
			}
			else cnt++,sum+=a[i],maxn=max(maxn,a[i]),ming=min(ming,a[i]); 
		}
	}
	t[p].sum=sum,t[p].cnt=cnt,t[p].maxn=maxn,t[p].ming=ming;
}

int build(int id,int l,int r){  //建树
	int p=++tot;
	t[p].l=l,t[p].r=r;
	if(r-l+1<=B){
		query(p,id,t[p].l,t[p].r);
		return p;
	}
	int mid=(l+r)>>1;
	t[p].ls=build(id,l,mid);
	t[p].rs=build(id,mid+1,r);
	pushup(p);
	return p;
}

void find(int id,int p,int val){
	if(t[p].r-t[p].l+1<=B){
		erase(p,id,t[p].l,t[p].r,val);   //注意这个地方只能把最小值改掉,而不能直接把所有 >x 的全 -x,不然后面打懒标记就重复减了
		return;		
	}
	pushdown(id,p);
	if(t[ls(p)].ming==val) find(id,t[p].ls,val);
	else find(id,t[p].rs,val);
	pushup(p);
}
void change1(int id,int p){  //情况 1 的区间修改
	if(t[p].cnt==0) return;  //空的,没有这个块中的值
	if(t[p].r-t[p].l+1<=B){  //直接暴力
		modify(p,id,t[p].l,t[p].r);
		return;
	}
	if(ll<=t[p].l&&t[p].r<=rr){
		while(t[p].cnt && t[p].ming-xx<L[id]) find(id,p,t[p].ming);	
		t[p].tag(-xx);  
		return;
	}
	pushdown(id,p);
	int mid=(t[p].l+t[p].r)>>1;
	if(ll<=mid) change1(id,t[p].ls);
	if(rr>mid) change1(id,t[p].rs);
	pushup(p);
}

void change2(int id,int p){  //情况 3 的区间修改,这个就很暴力了,如果当前区间的最大值 >xx 就直接往下递归,也不需要拆成 log 个子区间
	if(t[p].cnt==0 || t[p].maxn<=xx) return;
	if(t[p].r-t[p].l+1<=B){  //直接暴力
		modify(p,id,t[p].l,t[p].r);
		return;	
	}
	pushdown(id,p);
	int mid=(t[p].l+t[p].r)>>1;
	if(ll<=mid) change2(id,t[p].ls);
	if(rr>mid) change2(id,t[p].rs);
	pushup(p);
}

PIII merge(PIII x,PIII y){return {x.fi+y.fi,{max(x.se.fi,y.se.fi),min(x.se.se,y.se.se)}};}

PIII ask(int id,int p){
	if(t[p].r-t[p].l+1<=B){
		query(0,id,max(ll,t[p].l),min(rr,t[p].r));
		return {t[0].sum,{t[0].maxn,t[0].ming}};
	}
	if(ll<=t[p].l&&t[p].r<=rr) return {t[p].sum,{t[p].maxn,t[p].ming}};	
	pushdown(id,p);
	int mid=(t[p].l+t[p].r)>>1;
	PIII res={0,{0,inf}};
	if(ll<=mid) res=merge(res,ask(id,t[p].ls));
	if(rr>mid) res=merge(res,ask(id,t[p].rs));
	return res;
}

void Init(){
	L[0]=1,R[0]=1;
	while(true){
		++CNT;
		L[CNT]=R[CNT-1]+1,R[CNT]=min(1ll*V,1ll*L[CNT]*K-1ll);
		if(R[CNT]==V){break;}
	}
	
	for(int i=1;i<=n;i++) ID[i]=Get_id(a[i]);
	
	for(int i=0;i<=CNT;i++) rt[i]=build(i,1,n);
}

signed main(){
	n=read(),T=read();
	for(int i=1;i<=n;i++) a[i]=read();
	
	Init();
	
	int lstans=0;
	while(T--){
		int op=read();
		ll=read()^lstans,rr=read()^lstans;
		if(op==1){
			xx=read()^lstans;
			for(int i=0;i<=CNT;i++){
				if(xx<L[i]) change1(i,rt[i]);
				else if(xx>=L[i]&&xx<=R[i]) change2(i,rt[i]);  //注意是 <= 不是 < 因为这里是闭区间
			}
		}
		else{
			PIII ans={0,{0,inf}};
			for(int i=0;i<=CNT;i++){ans=merge(ans,ask(i,rt[i]));}
			lstans=ans.fi%mod;
			printf("%lld %d %d\n",ans.fi,ans.se.se,ans.se.fi);
		}
	}
	return 0;
}

闲话

这题之后我写过的代码长度的记录就不是 8.6K 了,而是 8.7K
说明:我比较喜欢写注释,所以我贴代码的时候去掉了大量影响观感的注释,所以代码本身其实没有这么长。

posted @ 2025-02-20 11:15  Green&White  阅读(13)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~
点击右上角即可分享
微信分享提示