学习笔记:莫队

莫队

【介绍】

  • 莫队\(\approx\)优化的暴力

【问题引入】

  • 序列,多次查询区间内不同的数的个数。

  • 序列长度和查询\(3 \times 10^4\)

【解决】

方法1:暴力1

  • 每次查询开一个桶,扫一遍查询区间,统计答案(若新增一个未出现过的数,则答案\(+1\))。

  • 时间复杂度:\(O(nq)\)

方法2:暴力2

  • 考虑利用上一次查询的结果。

  • 在上一次查询基础上,通过移动左右端点,得到当前查询的区间(删除一个数后,若其出现次数变为\(0\),则答案\(-1\))。

  • 时间复杂度:最差\(O(nq)\)

方法3:莫队

  • 在暴力\(2\)的基础上,通过调整查询的顺序,优化时间复杂度。

  • 将序列等分成\(\sqrt n\)块。

  • 将查询按左端点所在块编号排序(第一关键字),若左端点在同一块,则按右端点排序(第二关键字)。

  • 当n,q同级时,时间复杂度:\(O(n \sqrt n)\)

【莫队时间复杂度证明】

左端点:两次查询间的移动次数\(O(\sqrt n)\),共\(q\)次查询。所以总移动次数\(O(q \sqrt n )\)

右端点:对于每块内的查询,移动次数\(O(n)\),共\(\sqrt n\)块。所以总移动次数\(O(n \sqrt n)\)

【裸莫队代码】

#include <bits/stdc++.h>
using namespace std;
int n,m,len,l,r,ans;
int a[30005],b[30005],t[1000005],d[200005]; 
struct kkk
{
  int l,r,wz;
}q[200005];
bool cmp(kkk x,kkk y){return b[x.l]!=b[y.l] ? b[x.l]<b[y.l] : x.r<y.r;}
void add(int x)
{
  if(t[a[x]]==0) ans++;
  t[a[x]]++;
}
void del(int x)
{
  if(t[a[x]]==1) ans--;
  t[a[x]]--;
}
int main()
{
  scanf("%d",&n);
  len=sqrt(n);
  for(int i=1; i<=n; i++)
  {
  	scanf("%d",&a[i]);
  	b[i]=(i-1)/len+1;
  }
  scanf("%d",&m);
  for(int i=1; i<=m; i++)
  {
  	scanf("%d%d",&q[i].l,&q[i].r);
  	q[i].wz=i;
  }
  sort(q+1,q+1+n,cmp);
  l=1,r=0;
  for(int i=1; i<=n; i++)
  {
  	while(l<q[i].l)del(l++);
  	while(l>q[i].l)add(--l);
  	while(r>q[i].r)del(r--);
  	while(r<q[i].r)add(++r);
  	d[q[i].wz]=ans;
  }
  for(int i=1; i<=m; i++) printf("%d\n",d[i]);
  return 0;
}

回滚莫队

  • 有些题目在区间转移时,可能会出现增加或者删除无法实现的问题。在只有增加不可实现或者只有删除不可实现的时候,就可以使用回滚莫队在$ n \sqrt n$的时间内解决问题。

  • 回滚莫队的核心思想就是既然我只能实现一个操作,那么我就只使用一个操作,剩下的交给回滚解决。

  • 回滚莫队分为只使用增加操作的回滚莫队和只使用删除操作的回滚莫队。以下仅介绍只使用增加操作的回滚莫队,只使用删除操作的回滚莫队和只使用增加操作的回滚莫队只在算法实现上有一点区别,故不再赘述。


【具体算法】

  • 对原序列进行分块,对询问按以左端点所属块编号升序为第一关键字,右端点升序为第二关键字的方式排序。
  • 按顺序处理询问。
    • 如果询问左端点所属块\(B\)和上一个询问左端点所属块的不同,那么将莫队区间的左端点初始化为\(B\)的右端点加\(1\), 将莫队区间的右端点初始化为\(B\)的右端点。
    • 如果询问的左右端点所属的块相同,那么直接扫描区间回答询问。
    • 如果询问的左右端点所属的块不同。
      • 如果询问的右端点大于莫队区间的右端点,那么不断扩展右端点直至莫队区间的右端点等于询问的右端点。
      • 不断扩展莫队区间的左端点直至莫队区间的左端点等于询问的左端点。
      • 回答询问。
      • 撤销莫队区间左端点的改动,使莫队区间的左端点回滚到\(B\)的右端点加\(1\)

【题目】

传送门

【程序】

#include <bits/stdc++.h>
using namespace std;
const int N=1000100;
int n,m;
int l[N],r[N];
int a[N],b[N],ans[N],ANS;
struct BACK 
{ 
  int x,l,r,ans; 
}st[N]; 
int z=0;
void add(int x,int pos)
{
	st[++z]=(BACK){x,l[x],r[x],ANS};
	l[x]=min(l[x],pos);
	r[x]=max(r[x],pos);
	ANS=max(ANS,r[x]-l[x]);
}
struct LY { int l,r,id; }q[N];
bool ly(LY A,LY B){return b[A.l]!=b[B.l] ? b[A.l]<b[B.l] : A.r<B.r;}
int sl[N],sr[N];
int aa[N]; 
int main()
{
	scanf("%d",&n);
	for(int i=1; i<=n; i++)
		scanf("%d",&a[i]),aa[i]=a[i];
	sort(aa+1,aa+n+1);
	int nn=unique(aa+1,aa+n+1)-aa-1;
	for(int i=1; i<=n; i++)a[i]=lower_bound(aa+1,aa+nn+1,a[i])-aa;
	scanf("%d",&m);
	for(int i=1; i<=m; i++)
		scanf("%d%d",&q[i].l,&q[i].r),q[i].id=i;
	const int B=n/sqrt(m)+1;
	for(int i=1; i<=n; i++)
		b[i]=(i-1)/B+1;
	sort(q+1,q+m+1,ly);
	int L=1,R=0;
	memset(sl,127,sizeof(sl));
	memset(l,127,sizeof(l));
	for(int i=1; i<=m; i++)
	{
		if(b[q[i].l]!=b[q[i-1].l])
		{
			while(z)l[st[z].x]=1e9,r[st[z].x]=0,--z;
			L=b[q[i].l]*B+1,R=L-1;
			ANS=0;
		}
		if(b[q[i].l]==b[q[i].r])
		{
			for(int j=q[i].l; j<=q[i].r; j++)
			{
				sl[a[j]]=min(sl[a[j]],j);
				sr[a[j]]=max(sr[a[j]],j);
				ans[q[i].id]=max(ans[q[i].id],sr[a[j]]-sl[a[j]]);
			}
			for(int j=q[i].l; j<=q[i].r; j++)
				sl[a[j]]=1e9,sr[a[j]]=0;
			continue;
		}
		while(R<q[i].r) ++R,add(a[R],R);
		int pz=z;
		while(L>q[i].l) --L,add(a[L],L);
		ans[q[i].id]=ANS;
		while(z>pz) l[st[z].x]=st[z].l,r[st[z].x]=st[z].r,ANS=st[z].ans,--z;
		L=b[q[i].l]*B+1;
	}
	for(int i=1; i<=m; i++)
		printf("%d\n",ans[i]);
	return 0;
}
posted @ 2021-08-19 17:19  zeromclai  阅读(94)  评论(0)    收藏  举报