莫队进阶技巧——回滚与二次离线

1.回滚莫队

引入

回滚莫队,可以用来处理一些单向操作简单而反向操作较难的问题,如插入简单,而删除困难的问题。(如要求max,插入可以直接比较最大值,而删除需要找次大值)
即在莫队的四种移动边界的操作中,[l,r]可以快速转移到[l-1,r]或[l,r+1],而不能方便地转移到[l+1,r]和[l,r-1]。(或反之)
例题:AT1219 歴史の研究

题目:给你一个数列,求区间[l,r]中max(cnt[a[i]]*a[i]),其中cnt[]为出现次数,即求区间出现次数与大小的乘积最大值。多组询问。

我们可以记录当前区间[l,r]中每个数的出现次数,然后每次插入一个数时判断当前数的出现次数与大小的乘积,ans与其取max。
然而删除一个数就不能这样simply地删除,因为你删除后如果更改了最大值,你无法O(1)判断次大值,就不优雅了。
How to do it?

解题

在普通莫队中,我们将每个询问左端点所在的块编号作为第一关键字,将右端点作为第二关键字,然后进行排序。
而对于左端点在同一个块内的询问,左端点每次只会最多移动 O(\(\sqrt{n}\)) 次,总共 O(n\(\sqrt{n}\)) 次,右端点从左往右扫,一整个块是O(n)次,一共\(\sqrt{n}\)个块,所以也是O(n\(\sqrt{n}\))。
显然,对于右端点向右平移,本题很容易直接解决。
但对于左端点的秘技·反复横跳,普通莫队就力不从心了。
我们可以想,如果每次移动左端点时都是向左移插入而不会向右删除就好了。
我来!我会回滚!
设当前块为now,最右边是R[now]。
我们每次处理完当前块一个询问,就将左端点先移动到R[now]+1,然后再向左移动到下一个询问的左端点,就只有插入操作了。
但是我怎么知道左端点移动到R[now]+1时的答案呢?
以R[now]为界,我们将答案分为三类:

1.只在左半边的。
2.只在右半边的。
3.左右均有的。

对于1和3类,我们删除完就一定为0(因为没有左半边的)。

对于2类,我们只用在右指针向右移动时记录即可。(因此我们先移动右指针再移动左指针)。

SP.对于左右端点在同一块内,我们需暴力处理,容易发现单次时间不会超过O(\(\sqrt{n}\)),均摊O(n\(\sqrt{n}\))。

小结

1.我们将每个询问左端点所在的块编号作为第一关键字,将右端点作为第二关键字,然后进行排序。
2.对于左右端点在同一块内,我们暴力处理。
3.先将右指针移动到当前询问右端点,记录当前答案temp=ans。
4.再将左指针移动到当前询问左端点,记录当前答案ans,即当前询问答案。
5.我们将左指针回滚到当前块右端点+1,并将答案ans重新变回temp。
6.若下一个询问左端点所在的块不同,则将左指针移动到下一个询问左端点所在的块+1,右指针移动到左指针-1,并将ans赋值为0。

代码

#include<bits/stdc++.h>

using namespace std;
int n,m;
int a[200005],b[200005],id[200005];
int far[200005];
map<int,int>lsh;
int g;
struct question{
	int l,r;
	int id;
}q[200005];
int qg;
bool cmp(question x,question y){
	return (id[x.l]!=id[y.l]?id[x.l]<id[y.l]:x.r<y.r);
}
int cnt[200005];
void add(int u,long long &ans){
	cnt[u]++;
	ans=max(ans,1ll*cnt[u]*b[u]);
}
void Del(int u){
	cnt[u]--;
}
long long lans[200005];
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		if(!lsh[a[i]])lsh[a[i]]=++g;
		b[lsh[a[i]]]=a[i];
		a[i]=lsh[a[i]];
	}
	int siz=sqrt(n);
	for(int i=1;i<=n;i++)
		id[i]=i/siz,far[id[i]]=i;
	for(int i=1;i<=m;i++){
		int l,r;
		scanf("%d%d",&l,&r);
		if(id[l]==id[r]){
			long long ans=0;
			for(int j=l;j<=r;j++)
				add(a[j],ans);
			for(int j=l;j<=r;j++)
				cnt[a[j]]=0;
			lans[i]=ans;
		}
		else {
			q[++qg].id=i;
			q[qg].l=l;
			q[qg].r=r;
		}
	}
	sort(q+1,q+1+m,cmp);
	int l=far[id[q[1].l]]+1,r=l-1;
	long long ans,temp;
	ans=temp=0;
	for(int i=1;i<=m;i++){
		while(r<q[i].r){
			++r;
			add(a[r],ans);
		}
		temp=ans;
		while(l>q[i].l){
			l--;
			add(a[l],ans);
		}
		lans[q[i].id]=ans;
		ans=temp;
		while(l<far[id[q[i].l]]+1){
			Del(a[l]);
			l++;
		}
		if(i<m&&id[q[i+1].l]!=id[q[i].l]){
			while(r>=l){
				Del(a[r]);
				r--;
			}
			ans=temp=0;
			l=far[id[q[i+1].l]]+1,r=l-1;
		}
	}
	for(int i=1;i<=m;i++)printf("%lld\n",lans[i]);
	return 0;
}

例题

LG P5906 【模板】回滚莫队&不删除莫队

题目:给你一个数列,求区间[l,r]中最远两个相同的数之间的距离。多组询问。

题解:
显然我们记录当前区间每个数最左和最右出现位置是可以直接插入更新ans的。
而删除使用回滚莫队即可。
要注意的是,每次回滚完,为了保证当前答案一定只在右半边,需记录每种颜色右半边最左的位置。
代码:

#include<bits/stdc++.h>

using namespace std;
int n,m;
int a[200005],id[200005];
int far[200005];
map<int,int>lsh;
int g;
struct question{
	int l,r;
	int id;
}q[200005];
int qg;
bool cmp(question x,question y){
	return (id[x.l]!=id[y.l]?id[x.l]<id[y.l]:x.r<y.r);
}
int L[200005],R[200005],mL[200005],mR[200005];
void addL(int u,int w,int &ans){
	if(mL[u])ans=max(mL[u]-w,ans);
	else mL[u]=w;
	L[u]=w;
	if(R[u])ans=max(R[u]-w,ans);
}
void addR(int u,int w,int &ans){
	if(mR[u])ans=max(w-mR[u],ans);
	else mR[u]=w;
	if(L[u])ans=max(w-L[u],ans);
	R[u]=w;
}
int lans[200005];
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		if(!lsh[a[i]])lsh[a[i]]=++g;
		a[i]=lsh[a[i]];
	}
	int siz=sqrt(n);
	for(int i=1;i<=n;i++)
		id[i]=i/siz,far[id[i]]=i;
	scanf("%d",&m);
	for(int i=1;i<=m;i++){
		int l,r;
		scanf("%d%d",&l,&r);
		if(id[l]==id[r]){
			int ans=0;
			for(int j=r;j>=l;j--){
				addL(a[j],j,ans);
				if(!R[a[j]])addR(a[j],j,ans);
			}
			for(int j=l;j<=r;j++)
				L[a[j]]=R[a[j]]=0,mL[a[j]]=mR[a[j]]=0;
			lans[i]=ans;
		}
		else {
			q[++qg].id=i;
			q[qg].l=l;
			q[qg].r=r;
		}
	}
	sort(q+1,q+1+m,cmp);
	int l=far[id[q[1].l]]+1,r=l-1,ans,temp;
	ans=temp=0;
	for(int i=1;i<=m;i++){
		while(r<q[i].r){
			++r;
			addR(a[r],r,ans);
		}
		temp=ans;
		while(l>q[i].l){
			l--;
			addL(a[l],l,ans);
		}
		lans[q[i].id]=ans;
		ans=temp;
		while(l<far[id[q[i].l]]+1){
			L[a[l]]=0;
			mL[a[l]]=0;
			l++;
		}
		if(i<m&&id[q[i+1].l]!=id[q[i].l]){
			while(r>=l){
				R[a[r]]=0;
				mR[a[r]]=0;
				r--;
			}
			ans=temp=0;
			l=far[id[q[i+1].l]]+1,r=l-1;
		}
	}
	for(int i=1;i<=m;i++)printf("%d\n",lans[i]);
	return 0;
}

2.莫队二离

引入

普通莫队本质上是将O(n)个区间查询拆分成了O(n\(\sqrt{n}\))个移动边界的单点查询。
但是有的单点查询(或插入)并不能O(1)内求出来(如需要用树状数组等数据结构维护),所以时间复杂度不能均衡的分配。
而二离则是将移动边界的查询再次离线下来处理。

例题

LG P4887 【模板】莫队二次离线(第十四分块(前体))

题目:给你一个数列,求区间二元组(i,j)满足a[i]⊕a[j] == k的个数。多组询问(但k是相同的),k<=2^14,n<=1e5。

解题

首先我们想到用莫队离线下来,变成O(n\(\sqrt{n}\))个询问,每次求区间i∈[l,r]中满足 x⊕a[i] == k(x为当前数)
考虑到a⊕b == c等价于a⊕c == b(异或的基本性质),因此我们现预处理出恰好有k个1的所有数,发现不会超过C(14,7)个(也就3000多个)。
接下来我们只用用一个桶来进行储存各个值的个数即可查询。总复杂度O(n\(\sqrt{n}\) * C(14,7))。
然后就完美解………………个鬼,铁TLE好吧。
但是我们知道ans具有可减性。即\(\sum\limits_{i∈[l,r]}{[x⊕a[i] == k]}\) =\(\sum\limits_{i∈[1,r]}{[x⊕a[i] == k]}\) - \(\sum\limits_{i∈[1,l-1]}{[x⊕a[i] == k]}\)
而显然对于同一个前缀查询是一样的东西,所以我们可以把O(n\(\sqrt{n}\))个询问离线下来进行处理,放进对应前缀的vector,处理时进行查询。
这样我们的时间复杂度变为了O(n\(\sqrt{n}\) * C(14,7) + n),变高了呢。
我们发现插入时的复杂度是O(1),查询时的复杂度是O(C(14,7)),但是插入只有O(n)个,查询有(n\(\sqrt{n}\))个。
这分配的很不平均。
所以我们改成插入时枚举改变对应的满足要求的O(C(14,7))个桶内记录的值,查询时查询当前桶即可。
但是我们离线下来时空间复杂度时O(n\(\sqrt{n}\))的,怎么办?
容易发现对于原来同一个询问,移动的点是一段,把整段的左右端点丢入即可,即丢入vector的是一个区间而不是单点。

代码

#include<bits/stdc++.h>

using namespace std;
int n,m;
int a[100005],b[100005],id[100005];
long long nd[100005],nd2[100005];
struct LS{
	int id;
	int val;
}LSH[100005];
bool cmp1(LS x,LS y){
	return x.val<y.val;
}
int g;
struct question{
	int l,r;
	int id;
}q[100005];
int qg;
bool cmp(question x,question y){
	return (id[x.l]!=id[y.l]?id[x.l]<id[y.l]:((id[x.l]&1)?x.r>y.r:x.r<y.r));
}
int basket[200005];
struct nquestion{
	int l,r;
	int id;
	int type;
};
vector< nquestion > nqL[100005],nqR[100005];
long long lans[100005];
long long delta[100005];
int sum[200005];
int k;
int K[200005],kg;
void add(int u){
	for(int i=1;i<=kg;i++)
		sum[K[i]^u]++;
}
int main(){
	scanf("%d%d%d",&n,&m,&k);
	for(int i=0;i<=16834;i++){
		int now=i,g=0;
		while(now){
			if(now&1)g++;
			now>>=1;
		}
		if(g==k)K[++kg]=i;
	}
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	for(int i=1;i<=n;i++){
		nd[i]=sum[a[i]];
		add(a[i]);
		nd[i]+=nd[i-1];
	}
	memset(sum,0,sizeof(sum));
	for(int i=n;i>=1;i--){
		nd2[i]=sum[a[i]];
		add(a[i]);
		nd2[i]+=nd2[i+1];
	}
	memset(sum,0,sizeof(sum));
	nd2[0]=nd2[1];
	int siz=sqrt(n);
	for(int i=1;i<=n;i++)
		id[i]=i/siz;
	for(int i=1;i<=m;i++)
		scanf("%d%d",&q[i].l,&q[i].r),q[i].id=i;
	sort(q+1,q+1+m,cmp);
	int l=1,r=0;
	int dnm=0;
	for(int i=1;i<=m;i++){
		if(r<q[i].r){
			nqL[l].push_back((nquestion){r+1,q[i].r,i,-1});
			delta[i]+=nd[q[i].r]-nd[r];
			r=q[i].r;
		}
		if(r>q[i].r){
			nqL[l].push_back((nquestion){q[i].r+1,r,i,1});
			delta[i]-=nd[r]-nd[q[i].r];
	 		r=q[i].r;
		}
		if(l<q[i].l){
			nqR[r].push_back((nquestion){l,q[i].l-1,i,1});
			delta[i]-=nd2[l]-nd2[q[i].l];
			l=q[i].l;
		}
		if(l>q[i].l){
			nqR[r].push_back((nquestion){q[i].l,l-1,i,-1});
			delta[i]+=nd2[q[i].l]-nd2[l];
			l=q[i].l;
		}
	}
	for(int i=1;i<=n;i++){
		for(int j=0;j<nqL[i].size();j++){
			for(int k=nqL[i][j].l;k<=nqL[i][j].r;k++)
				delta[nqL[i][j].id]+=1ll*nqL[i][j].type*sum[a[k]];
		}
		add(a[i]);
	}
	memset(sum,0,sizeof(sum));
	for(int i=n;i>=1;i--){
		for(int j=0;j<nqR[i].size();j++){
			for(int k=nqR[i][j].l;k<=nqR[i][j].r;k++)
				delta[nqR[i][j].id]+=1ll*nqR[i][j].type*sum[a[k]];
		}
		add(a[i]);
	}
	for(int i=1;i<=m;i++)
		delta[i]+=delta[i-1],lans[q[i].id]=delta[i];
	for(int i=1;i<=m;i++)printf("%lld\n",lans[i]);
	return 0;
}
posted @ 2021-11-12 21:56  天穹の流星  阅读(453)  评论(0)    收藏  举报