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

题解

题目传送门

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

看修改,对于块 \([2^k,2^{k+1})\)

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

  2. \(2^{k+1}\le x\):啥都不用做。

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

总时间复杂度是 \(O((n+m)\log n \log V)\),空间复杂度是 \(O(n\log V)\)

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

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

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

当然 ynoi 不卡常是不可能的。
主要就是改两个块长。
倍增值域分块的底数取 \(2\) 其实是基本没希望的,而且空间是贴着过去的(实测 \(59MB\))。
如果我们按照 \([1,K),[K,K^2),[K^2,K^3),...\) 这样分:
会分成 \(\log_K V\) 个块,每个数只会掉 \(\log_K V\) 次,在情况 \(3\) 中的数至多减 \(K-1\) 次就会掉到下面的块。
所以情况 \(1\) 的复杂度会变成 \(O(n \log_K V \log n)\),情况 \(3\) 的复杂度会变成 \(O(n K \log_K V \log n)\)
对应的如果要让空间为 \(O(n)\)\(B\) 要取 \(O(\log_K V)\)
虽然当 \(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;
}
posted @ 2025-02-20 11:15  Green&White  阅读(200)  评论(0)    收藏  举报