【8*】莫队学习笔记

前言

此类知识点大纲中并未涉及,所以【8】是我自己的估计,后带星号表示估计,仅供参考。

莫队主要运用的是离线处理思想,是离线处理思想最经典的体现。离线处理思想在大纲中的评价是 8 级,所以我认为离线处理思想就是指莫队,就评价 8 级。

由于学习间隔较长,所以可能代码码风不太相同,尽情谅解。

什么线段树,平衡树,树套树,我分块/莫队 n 暴力才是 yyds!

普通莫队

对于与区间中各个元素有关的区间查询,且题目允许离线处理,我们可以考虑莫队算法

首先,考虑比较朴素的做法。对于第 i 个询问的区间 li,ri,我们考虑把上一个区间的边界 li1,ri1 移动到 li,ri,并在移动的过程中处理增加,删除一个元素的影响。

但是这样做复杂度很高,若序列大小与询问次数同阶,复杂度为 O(n2),无法接受。

我们想到,在移动左右指针的过程中,有一些元素被重复计算了。那我们能否通过一些特殊的顺序来依次处理这些询问,来尽量减少重复计算次数呢?当然可以。

我们把序列分块,分成 n 个块。对于每一个询问,以左端点所在的块号升序排序,同一块中按照右端点排序。排完序之后,在如上所述移动指针,复杂度即为 O(nn)

bool cmp(struct ask a,struct ask b)
{
	return ((a.l-1)/k==(b.l-1)/k)?(a.r<b.r):(a.l<b.l);
}

时间复杂度证明:

假设序列大小与询问次数同阶,均为 n。对于左端点的移动,由于每次都是在同一块或连续的两块中移动,所以每次左端点的移动复杂度为 O(n),总共有 n 个询问,总体复杂度就是 O(nn)。对于右端点的移动,每块中右端点单升不降,最多会移动 n 次。总共有 n 个块,总体复杂度也是 O(nn)。忽略常数,莫队算法的时间复杂度就是 O(nn)

具体实现的时候,我们可以单独写一个 add 函数来统计增加元素的影响,单独写一个 del 函数来统计删除元素的影响。同时,还需要记录每个询问原本的位置,方便按顺序输出。

for(int i=1;i<=m;i++)
    {
    while(q[i].l<pl)pl--,add(a[pl]);
    while(q[i].l>pl)del(a[pl]),pl++;
    while(q[i].r<pr)del(a[pr]),pr--;
    while(q[i].r>pr)pr++,add(a[pr]);
    ans[q[i].p]=tol;
    }

注意,代码中维护的区间为 [pl,pr],由于第一次操作处理的方式为在区间 [1,r] 减去区间 [1,l1] 的影响,pl 是先处理影响再减,pr 是先加再处理影响,所以 pl 的初值为 1pr 的初值为 0。如果不想处理这么多边界情况,可以试试例题 1 中较为复杂的写法。

例题 1

P1494 [国家集训队] 小 Z 的袜子

莫队模板题。每次增加元素时,撤销这个元素的数量对符合要求的选法造成的影响,将这个元素的数量加一,重新计算这个元素的数量对符合要求的选法造成的影响。删除也是同理。需要简单的组合数学知识。

这里采用的是较为复杂的莫队写法,这样写时需要注意每种情况判断的顺序。(由于没写 adddel 函数,码风有点烂)

#include <bits/stdc++.h>
using namespace std;
struct ask
{
	long long l,r,p;
}q[50010];
long long n,m,k,a[50010],son[50010],ma[50010],ans[50010],id[50010],sum[50010],now=1,lim=0,prel=0,prer=0; 
bool cmp1(struct ask a,struct ask b)
{
	return a.l<b.l;
}

bool cmp2(struct ask a,struct ask b)
{
	return a.r<b.r;
}

long long gcd(long long x,long long y)
{
	if(y==0)return x;
	else return gcd(y,x%y);
}

int main()
{
	scanf("%lld%lld",&n,&m);
	k=sqrt(n);
	for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
	for(int i=1;i<=m;i++)
	    {
	    scanf("%lld%lld",&q[i].l,&q[i].r);
	    q[i].p=i;
	    }
	sort(q+1,q+m+1,cmp1);
	for(int i=1;i<=m;i++)
	    {
	    id[i]=(q[i].l-1)/k+1;
	    sum[id[i]]++;
	    }
	lim=id[m];
	for(int i=1;i<=lim;i++)
	    {
	    sort(q+now,q+sum[i]+now,cmp2);
	    now+=sum[i];
	    }
	now=0;
	for(int i=1;i<=m;i++)
	    {
	    	if(q[i].l==q[i].r)son[q[i].p]=0,ma[q[i].p]=1,id[i]=0;
	    	else if(id[i]!=id[i-1])
	    	    {
	    	    for(int j=1;j<=50001;j++)ans[j]=0;
	    	    now=0;
	    	    for(int j=q[i].l;j<=q[i].r;j++)
	    	        ans[a[j]]++;
	    	    for(int j=1;j<=50001;j++)
	    	        now+=ans[j]*(ans[j]-1)/2;
	    	    prel=q[i].l,prer=q[i].r,son[q[i].p]=now,ma[q[i].p]=(q[i].r-q[i].l+1)*(q[i].r-q[i].l)/2;
	    	    if(son[q[i].p]==0)ma[q[i].p]=1;
	    	    else 
	    	       {
	    	       	long long d=gcd(son[q[i].p],ma[q[i].p]);
	    	       	son[q[i].p]/=d;ma[q[i].p]/=d;
				   }
	    	    }
	    	else 
	    	   {
	    	   	if(q[i].l<prel)
	    	   	   for(int j=q[i].l;j<prel;j++)
	    	   	       now-=ans[a[j]]*(ans[a[j]]-1)/2,ans[a[j]]++,now+=ans[a[j]]*(ans[a[j]]-1)/2;
	    	   	else if(q[i].l>prel)
	    	   	   for(int j=prel;j<q[i].l;j++)
	    	   	       now-=ans[a[j]]*(ans[a[j]]-1)/2,ans[a[j]]--,now+=ans[a[j]]*(ans[a[j]]-1)/2;
	    	   	for(int j=prer+1;j<=q[i].r;j++)
	    	   	       now-=ans[a[j]]*(ans[a[j]]-1)/2,ans[a[j]]++,now+=ans[a[j]]*(ans[a[j]]-1)/2;
	    	   	prel=q[i].l,prer=q[i].r,son[q[i].p]=now,ma[q[i].p]=(q[i].r-q[i].l+1)*(q[i].r-q[i].l)/2;
	    	   	if(son[q[i].p]==0)ma[q[i].p]=1;
	    	    else 
	    	       {
	    	       	long long d=gcd(son[q[i].p],ma[q[i].p]);
	    	       	son[q[i].p]/=d;ma[q[i].p]/=d;
				   }
			   }
	    }
	for(int i=1;i<=m;i++)
	    printf("%lld/%lld\n",son[i],ma[i]);
    return 0;
}

例题 2

P2709 小B的询问

同例题 1,改一改统计贡献的方式即可。

#include <bits/stdc++.h>
using namespace std;
struct ask
{
	long long l,r,p;
}q[50010];
long long n,m,k,a[50010],ans[50010],ansd[50010],id[50010],sum[50010],now=1,lim=0,prel=0,prer=0; 
bool cmp1(struct ask a,struct ask b)
{
	return a.l<b.l;
}

bool cmp2(struct ask a,struct ask b)
{
	return a.r<b.r;
}

int main()
{
	scanf("%lld%lld%lld",&n,&m,&k);
	k=sqrt(n);
	for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
	for(int i=1;i<=m;i++)
	    {
	    scanf("%lld%lld",&q[i].l,&q[i].r);
	    q[i].p=i;
	    }
	sort(q+1,q+m+1,cmp1);
	for(int i=1;i<=m;i++)
	    {
	    id[i]=(q[i].l-1)/k+1;
	    sum[id[i]]++;
	    }
	lim=id[m];
	for(int i=1;i<=lim;i++)
	    {
	    sort(q+now,q+sum[i]+now,cmp2);
	    now+=sum[i];
	    }
	now=0;
	for(int i=1;i<=m;i++)
	    {
	    	if(id[i]!=id[i-1])
	    	    {
	    	    for(int j=1;j<=50001;j++)ans[j]=0;
	    	    now=0;
	    	    for(int j=q[i].l;j<=q[i].r;j++)
	    	        ans[a[j]]++;
	    	    for(int j=1;j<=50001;j++)
	    	        now+=ans[j]*ans[j];
	    	    prel=q[i].l,prer=q[i].r,ansd[q[i].p]=now;
	    	    }
	    	else 
	    	   {
	    	   	if(q[i].l<prel)
	    	   	   for(int j=q[i].l;j<prel;j++)
	    	   	       now-=ans[a[j]]*ans[a[j]],ans[a[j]]++,now+=ans[a[j]]*ans[a[j]];
	    	   	else if(q[i].l>prel)
	    	   	   for(int j=prel;j<q[i].l;j++)
	    	   	       now-=ans[a[j]]*ans[a[j]],ans[a[j]]--,now+=ans[a[j]]*ans[a[j]];
	    	   	for(int j=prer+1;j<=q[i].r;j++)
	    	   	       now-=ans[a[j]]*ans[a[j]],ans[a[j]]++,now+=ans[a[j]]*ans[a[j]];
	    	   	prel=q[i].l,prer=q[i].r,ansd[q[i].p]=now;
			   }
	    }
	for(int i=1;i<=m;i++)
	    printf("%lld\n",ansd[i]);
    return 0;
}

例题 3

P4396 [AHOI2013] 作业

由于与区间内每个数字出现的次数有关,可以离线,考虑莫队维护每个数字出现的次数 cnt

对于每一个数字,维护一个 c 表示是否出现这种数字。当我们发现有一个数字第一次出现,也就是增加前 cnt0 时,将这个数字对应的 c 改为 1。当我们发现有一个数字被全部删除,也就删除后是 cnt0 时,将这个数字对应的 c 改为 0

询问时,第一问本质上就是求值域 [a,b] 内的数的出现次数,就是在 cnt 数组上查询 [a,b] 的区间和。第二问本质上就是求值域 [a,b] 内的数的种类个数,就是在 c 数组上查询 [a,b] 的区间和。由于都是单点修改,区间查询,考虑用一个树状数组维护,时间复杂度为 O(nnlogn)

注意特判 a>b 的情况,否则树状数组会出错。

题解区有复杂度更优秀的做法,可以补充学习(题解区的做法较为常用)。

#include <bits/stdc++.h>
using namespace std;
struct ask
{
	int l,r,a,b,p;
}e[200000];
int n,m,k,prel,prer,a[200000],cnt[200000],c1[200000],c2[200000],ans1[200000],ans2[200000],id[200000],num[200000];
bool cmp1(struct ask a,struct ask b)
{
	return a.l<b.l;
}

bool cmp2(struct ask a,struct ask b)
{
	return a.r<b.r;
}

int lowbit(int x)
{
	return x&(-x);
}

void add(int x,int d,int c[])
{
	while(x<=110000)c[x]+=d,x+=lowbit(x);
}

int ask(int x,int c[])
{
	int ans=0;
	while(x>0)ans+=c[x],x-=lowbit(x);
	return ans;
}

int main()
{
	scanf("%d%d",&n,&m);
	k=sqrt(n);
	for(int i=1;i<=n;i++)
	    scanf("%d",&a[i]);
	for(int i=1;i<=m;i++)
	     {
	     	scanf("%d%d%d%d",&e[i].l,&e[i].r,&e[i].a,&e[i].b);
	     	e[i].p=i;
		 }
	sort(e+1,e+m+1,cmp1);
	for(int i=1;i<=m;i++)
	    {
	    	id[i]=(e[i].l-1)/k+1;
	    	num[id[i]]++;
		}
	int now=1,lim=id[m];
	for(int i=1;i<=lim;i++)
	    {
	    	sort(e+now,e+now+num[i],cmp2);
	    	now+=num[i];
		}
	for(int i=1;i<=m;i++)
	    {
	    	if(e[i].a>e[i].b)ans1[e[i].p]=0,ans2[e[i].p]=0,id[i]=0;
	    	else if(id[i]!=id[i-1])
	    	     {
	    	     	memset(cnt,0,sizeof(cnt));memset(c1,0,sizeof(c1));memset(c2,0,sizeof(c2));
	    	     	for(int j=e[i].l;j<=e[i].r;j++)
	    	     	       {
	    	     	       if(cnt[a[j]]==0)add(a[j],1,c2);
	    	     	       cnt[a[j]]++;
						   add(a[j],1,c1);
					       }
					ans1[e[i].p]=ask(e[i].b,c1)-ask(e[i].a-1,c1);
					ans2[e[i].p]=ask(e[i].b,c2)-ask(e[i].a-1,c2);
					prel=e[i].l;prer=e[i].r;
				 }
			else
			   {
			   	if(e[i].l<prel)
			   	   {
			   	   for(int j=e[i].l;j<prel;j++)
	    	     	   {
	    	     	   if(cnt[a[j]]==0)add(a[j],1,c2);
	    	     	   cnt[a[j]]++;
					   add(a[j],1,c1);
					   }
				   }
				else if(e[i].l>prel)
				   {
				   for(int j=prel;j<e[i].l;j++)
	    	     	   {
	    	     	   cnt[a[j]]--;
	    	     	   if(cnt[a[j]]==0)add(a[j],-1,c2);
					   add(a[j],-1,c1);
					   }
					}
				for(int j=prer+1;j<=e[i].r;j++)
	    	     	{
	    	     	if(cnt[a[j]]==0)add(a[j],1,c2);
	    	     	cnt[a[j]]++;
					add(a[j],1,c1);
					}
				ans1[e[i].p]=ask(e[i].b,c1)-ask(e[i].a-1,c1);
				ans2[e[i].p]=ask(e[i].b,c2)-ask(e[i].a-1,c2);
				prel=e[i].l;prer=e[i].r;
			   }
		}
	for(int i=1;i<=m;i++)
	    printf("%d %d\n",ans1[i],ans2[i]);
    return 0;
}

例题 4

AT_tenka1_2014_final_d 高橋君

由于题目多次查询 n,k,数据范围 n,m105,形式上很像莫队。我们再深入考虑一下,发现确实可以通过询问移动指针的方式来维护每次查询的 n,k,考虑莫队。

对于 n,kn,k+1n,kn,k1,预处理阶乘和阶乘逆元后,直接将答案加上 Cnk+1 或减去 Cnk 即可。

对于 n,kn+1,k,需要推一下式子。记 g=i=0kCn+1i,f=i=0kCni

【6】组合计数学习笔记 中我们知道,组合数有这样一个性质:

Cmn=Cm1n+Cm1n1

所以,我们可以推出这样一个式子:

g=i=0kCn+1i=(i=1kCni+Cni1)+1

我们发现,对于每个 i(1i<k),其在 ii+1 时都计算了一次,总共计算了两次,所以可得:

i=1k(Cni+Cni1)+1=(i=1k12Cni)+Cn0+Cnk+1

注意到 Cn0=1,所以可以把后面的 +1 变成 +Cn0,这样 Cn0 也计算了两次,可以放进前面的式子里:

(i=1k12Cni)+Cn0+Cnk+1=(i=0k12Cni)+Cnk

我们发现,(i=0k12Cni) 在除系数外和 f 只有 kk1 的差异,所以考虑加上 2Cnk,在求和的后面减去,即:

(i=0k12Cni)+Cnk=(i=0k2Cni)Cnk

将系数提出来,把 f 换进去得:

(i=0k2Cni)Cnk=2(i=0kCni)Cnk=2fCnk=g

这样,我们就得到了 n,kn+1,k 的关系式,可以利用其求出改变后的结果。对于 n,kn1,k,只需要把这个式子反过来,改为 f=g+Cnk2。注意需要处理 2 的逆元。

这里使用的是莫队较为简单的写法,注意边界情况。

这启示我们:莫队不只能处理序列问题,对于每一个多次询问,询问有两个元素的问题,如果能从其中每一个元素推到变化 1 之后,就可以考虑莫队。

#include <bits/stdc++.h>
using namespace std;
struct ask
{
	long long l,r,p;
}q[200000];
long long n,k,it=0,jc[200000],inv[200000],ans[200000],sum=0,pl=1,pr=0,mod=1000000007;
bool cmp(struct ask a,struct ask b)
{
	return ((a.l-1)/k==(b.l-1)/k)?(a.r<b.r):(a.l<b.l);
}

long long power(long long a,long long p,long long m)
{
	long long ans=1,x=a;
	while(p)
	   {
	   	if(p%2==1)ans=ans*x%m;
	   	p/=2;
	   	x=x*x%m;
	   }
	return ans;
}

long long get_c(long long x,long long y)
{
	if(x<y)return 0;
	return jc[x]*inv[x-y]%mod*inv[y]%mod;
}

void add(long long x,long long y)
{
	sum=(sum+get_c(x,y)%mod)%mod;
}

void del(long long x,long long y)
{
	sum=(sum-get_c(x,y)%mod+mod)%mod;
}

void upd(long long x,long long y)
{
	sum=(sum*2%mod-get_c(x-1,y-1)+mod)%mod;
}

void dpd(long long x,long long y)
{
	sum=((sum+get_c(x-1,y-1))%mod*it%mod)%mod;
}

int main()
{
	scanf("%lld",&n);
	k=sqrt(100000);
	inv[0]=jc[0]=1;it=power(2,mod-2,mod);
	for(long long i=1;i<=100000;i++)jc[i]=(jc[i-1]*i)%mod;
	for(long long i=1;i<=100000;i++)inv[i]=power(jc[i],mod-2,mod);
	for(long long i=1;i<=n;i++)
	    {
	    	scanf("%lld%lld",&q[i].l,&q[i].r);
	    	q[i].p=i;
		}
	sort(q+1,q+n+1,cmp);
	for(long long i=1;i<=n;i++)
	    {
	    	while(pr<q[i].r+1)add(pl,pr),pr++;
	    	while(pr>q[i].r+1)pr--,del(pl,pr);
	    	while(pl<q[i].l)pl++,upd(pl,pr);
	    	while(pl>q[i].l)dpd(pl,pr),pl--;
	    	ans[q[i].p]=sum;
		}
	for(long long i=1;i<=n;i++)
	    printf("%lld\n",ans[i]);
	return 0;
}

带修莫队

莫队不仅能维护查询操作,甚至也可以处理一些修改操作。

处理方式就是增加一个时间维度。对于每一次修改,我们将这次修改及这次修改后所有操作的时间增加 1。也就是说,时间相当于在这次操作之前(包括这个操作)有多少个修改操作。

如果我们可以从 (l,r,t) 推到 (l,r,t+1)(l,r,t1),那么我们就可以通过莫队的方式来处理这些修改造成的影响。

我们按照 l,r 进行分块,块内按照 t 递增排序。

bool cmp(struct ask a,struct ask b)
{
	return ((a.l-1)/k==(b.l-1)/k)?(((a.r-1)/k==(b.r-1)/k)?(a.t<b.t):(a.r<b.r)):(a.l<b.l);
}

注意此处块长应该取 n23,这样才能达到较好的 O(n53) 复杂度。具体证明我不会,记下来即可,或者参考文末的博客。

例题 5

P1903 [国家集训队] 数颜色 / 维护队列

考虑莫队。对于每一个数字,维护一个 cnt 表示这种数字出现的次数。当我们发现有一个数字第一次出现,也就是增加前 cnt0 时,将最终的答案加 1。当我们发现有一个数字被全部删除,也就删除后是 cnt0 时,将最终的答案减 1

然后是对时间的操作。当时间增加时,我们按照修改操作,把序列中对应数值改为修改的数值。注意如果这次修改的数字在当前区间,已经统计的 l,r 中时,并没有计算该部分的贡献,需要 add 新增的数值,del 之前的数值来统计贡献。

对于时间减少,只需要逆向做一次时间增加即可,时间增加时需要记录之前的数值。这里采用了一个比较巧妙的方法,时间增加时 swap 序列中的数值和修改操作中的数值,这样在时间减少刚好逆转过来,再 swap 回去,又回到没有时间增加的样子。

#include <bits/stdc++.h>
using namespace std;
struct ask
{
	int l,r,t,p;
}q[200000],g[200000];
int n,m,k,l,r,mg,mq,a[200000],cnt[2000000],tol=0,ans[200000],pl=1,pr=0,pt=0;
char op;
bool cmp(struct ask a,struct ask b)
{
	return ((a.l-1)/k==(b.l-1)/k)?(((a.r-1)/k==(b.r-1)/k)?(a.t<b.t):(a.r<b.r)):(a.l<b.l);
}

void add(int x)
{
	if(cnt[x]==0)tol++;
	cnt[x]++;
}

void del(int x)
{
	cnt[x]--;
	if(cnt[x]==0)tol--;
}

void upd(int t,int now)
{
	if(g[t].l>=q[now].l&&g[t].l<=q[now].r)
	   {
	   	del(a[g[t].l]);
	   	add(g[t].r);
	   }
	swap(a[g[t].l],g[t].r);
}
 
int main()
{
	scanf("%d%d",&n,&m);
	k=pow(n,0.6666);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	for(int i=1;i<=m;i++)
	    {
	    	cin>>op>>l>>r;
	    	if(op=='R')g[++mg].l=l,g[mg].r=r;
	    	else if(op=='Q')q[++mq].l=l,q[mq].r=r,q[mq].t=mg,q[mq].p=mq;
		}
	sort(q+1,q+mq+1,cmp);
	for(int i=1;i<=mq;i++)
	    {
	    	while(q[i].l<pl)pl--,add(a[pl]);
	    	while(q[i].l>pl)del(a[pl]),pl++;
	    	while(q[i].r<pr)del(a[pr]),pr--;
	    	while(q[i].r>pr)pr++,add(a[pr]);
	    	while(q[i].t<pt)upd(pt,i),pt--;
	    	while(q[i].t>pt)pt++,upd(pt,i);
	    	ans[q[i].p]=tol;
		}
	for(int i=1;i<=mq;i++)
	    printf("%d\n",ans[i]);
	return 0;
}

回滚莫队

有的时候,莫队维护的东西不适合删除操作,或删除操作很复杂,比如最大值,这个时候就需要使用回滚莫队来避免删除。

普通莫队排序后,左端点 l 同一块内右端点 r 单升不降,自然不需要删除。而对于左端点 l,我们可以每次回滚到左端点所在的块的末尾,然后再增加回去。注意回滚时是撤销影响而不是删除,比如维护数字 i 出现的次数 cnti 的最大值,删除就是 cnti1,并更新最大值,撤销就是 cnti1,没有别的操作。

注意,如果 l,r 在同一块内,这样子就不对。但是如果 l,r 在同一块内,直接更新即可。

由于左端点 l 每次更新依旧维持在 n 的范围内,所以时间复杂度依旧是 O(nn)

回滚莫队一般步骤

1:若 l,r 处于同一块内,直接单独处理。

2:如果当前询问与上一个询问左端点 l 所在的块不同,清空,朴素处理这个询问。

3:处理右端点的扩展。

4:保存需要回滚的值。(也就是左端点所在的块的末尾到右端点这段区间的信息)

5:处理左端点的扩展。

6:计算答案。

7:撤销左端点的扩展的影响。

8:回滚保存的值。

例题 6

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

离散化,考虑莫队维护每个元素最左边的位置 lc 和最右边的位置 rc。由于需要维护最大值,考虑回滚莫队。记 ans 为答案。

1:若 l,r 处于同一块内,直接单独处理。每次增加元素时,更新 lcrc,并用 rclcans 取最大值。

2:如果当前询问与上一个询问左端点 l 所在的块不同,清空 lc,rcans

3:处理右端点的扩展,更新 lcrc,并用 rclcans 取最大值。

4:保存需要回滚的值,ans。因为我们撤销左端点的扩展的影响,没办法实时更新 ans 的值,不然就需要维护次大值,次次大值,次次次大值 复杂度直接爆炸,所以我们先保存没有处理左端点的扩展前的 ans 用于撤销影响。

5:处理左端点的扩展,更新 lcrc,并用 rclcans 取最大值。

6:计算答案,直接记录 ans 到询问对应位置。

7:撤销左端点的扩展的影响,在扩展时记录 gl,gr,如果在位置 i 更新了 lcrc,那么对应的 gli,gri 改为更新之前的值,表示如果没有这个点的 lc。撤销时如果点 i 被撤销,就将这个点的值对应的 lcrc 改为对应的 gli,gri

8:回滚保存的值。由于左端点的扩展已经撤销,需要更新 ans 值,把之前保存的回滚值赋给 ans。这样,经过 7,8 两步,记录的信息就又变成了左端点所在的块的末尾到右端点这段区间的信息。

#include <bits/stdc++.h>
using namespace std;
struct ask
{
	long long l,r,p;
}q[200010];
struct node
{
	long long v,p;
}b[200010];
long long n,m,k,pl,pr,a[200010],lc[200010],rc[200010],gl[200010],gr[200010],ans[200010],lt[200010],rt[200010],tol=0,mx=0,mp=0,ms=0,flag=0;
bool cmp1(struct node a,struct node b)
{
	return a.v<b.v;
}

bool cmp2(struct ask c,struct ask d)
{
	return ((c.l-1)/k==(d.l-1)/k)?(c.r<d.r):(c.l<d.l);
}

void add(long long x)
{
	if(x<lc[a[x]])gl[x]=lc[a[x]],lc[a[x]]=x;
	if(x>rc[a[x]])gr[x]=rc[a[x]],rc[a[x]]=x;
	if(lc[a[x]]!=99999999&&rc[a[x]]!=0&&lc[a[x]]!=rc[a[x]])mx=max(mx,rc[a[x]]-lc[a[x]]);
}

void del(long long x)
{
	if(lc[a[x]]==x)lc[a[x]]=gl[x];
	if(rc[a[x]]==x)rc[a[x]]=gr[x];
}

int main()
{
	scanf("%lld",&n);
	k=sqrt(n);
	for(long long i=1;i<=n;i++)
	     {
	     scanf("%lld",&b[i].v);
	     b[i].p=i;
	     }
	sort(b+1,b+n+1,cmp1);
	for(long long i=1;i<=n;i++)
	    {
	    	if(b[i-1].v!=b[i].v)tol++;
	    	a[b[i].p]=tol;
		}
	scanf("%lld",&m);
	for(long long i=1;i<=m;i++)
	    {
	    scanf("%lld%lld",&q[i].l,&q[i].r);
	    q[i].p=i;
	    }
	sort(q+1,q+m+1,cmp2);
	for(long long i=1;i<=m;i++)
	    {
		long long l=q[i].l,r=q[i].r;
		if((l-1)/k==(r-1)/k)
	       {
	       	ms=0;
	       	for(long long j=l;j<=r;j++)lt[a[j]]=99999999,rt[a[j]]=0;
	       	for(long long j=l;j<=r;j++)
	       	    {
	               if(j<lt[a[j]])lt[a[j]]=j;
	               if(j>rt[a[j]])rt[a[j]]=j;
	               if(lt[a[j]]!=99999999&&rt[a[j]]!=0&&lt[a[j]]!=rt[a[j]])ms=max(ms,rt[a[j]]-lt[a[j]]);
			    }
	       	ans[q[i].p]=ms;
         	if((l-1)/k!=(q[i-1].l-1)/k||i==1)flag=1;
	       	pl=l,pr=r;
		   }
		else if((l-1)/k!=(q[i-1].l-1)/k||i==1||flag)
		   {
		   for(long long j=1;j<=n;j++)lc[j]=99999999,rc[j]=0;
		   mx=0,flag=0;
		   long long ed=((l-1)/k+1)*k;
		   for(long long j=((l-1)/k+1)*k+1;j<=r;j++)add(j);
		   mp=mx;
		   for(long long j=l;j<=ed;j++)add(j);
	       ans[q[i].p]=mx;
	       for(long long j=l;j<=ed;j++)del(j);
	       pl=l,pr=r;
	       }
	    else
	       {
	       mx=mp;
	       for(long long j=pr+1;j<=r;j++)add(j);
	       mp=mx;
	       long long ed=((l-1)/k+1)*k;
	       for(long long j=l;j<=ed;j++)add(j);
	       ans[q[i].p]=mx;
	       for(long long j=l;j<=ed;j++)del(j);
	       pl=l,pr=r;
		   }
		}
	for(long long i=1;i<=m;i++)printf("%lld\n",ans[i]);
	return 0;
}

例题 7

AT_joisc2014_c 歴史の研究

离散化,记录离散化后每个数值对应的初始值。考虑莫队维护每个元素出现的次数 cnt。由于需要维护最大值,考虑回滚莫队。记 ans 为答案。

1:若 l,r 处于同一块内,直接单独处理。每次增加元素时,令对应的 cnt 的值增加 1,然后用这个元素对应的初始值乘以对应的 cnt,与 ans 取最大值。

2:如果当前询问与上一个询问左端点 l 所在的块不同,清空 cntans

3:处理右端点的扩展,令对应的 cnt 的值增加 1,然后用这个元素对应的初始值乘以对应的 cnt,与 ans 取最大值。

4:保存需要回滚的值,ans。因为我们撤销左端点的扩展的影响,没办法实时更新 ans 的值,不然就需要维护次大值,次次大值,次次次大值 复杂度直接爆炸,所以我们先保存没有处理左端点的扩展前的 ans 用于撤销影响。

5:处理左端点的扩展,令对应的 cnt 的值增加 1,然后用这个元素对应的初始值乘以对应的 cnt,与 ans 取最大值。

6:计算答案,直接记录 ans 到询问对应位置。

7:撤销左端点的扩展的影响,删除元素时,直接将对应的 cnt1 即可。

8:回滚保存的值。由于左端点的扩展已经撤销,需要更新 ans 值,把之前保存的回滚值赋给 ans。这样,经过 7,8 两步,记录的信息就又变成了左端点所在的块的末尾到右端点这段区间的信息。

#include <bits/stdc++.h>
using namespace std;
struct ask
{
	long long l,r,p;
}q[200000];
struct node
{
	long long v,p;
}b[200000];
long long n,m,k,pl,pr,a[200000],y[200000],cnt[200000],ans[200000],tp[200000],tol=0,mx=0,mp=0,ms=0,flag=0;
bool cmp1(struct node a,struct node b)
{
	return a.v<b.v;
}

bool cmp2(struct ask c,struct ask d)
{
	return ((c.l-1)/k==(d.l-1)/k)?(c.r<d.r):(c.l<d.l);
}

void add(long long x)
{
	cnt[a[x]]++;
	if(cnt[a[x]]*y[a[x]]>mx)mx=cnt[a[x]]*y[a[x]];
}

int main()
{
	scanf("%lld%lld",&n,&m);
	k=sqrt(n);
	for(long long i=1;i<=n;i++)
	     {
	     scanf("%lld",&b[i].v);
	     b[i].p=i;
	     }
	sort(b+1,b+n+1,cmp1);
	for(long long i=1;i<=n;i++)
	    {
	    	if(b[i-1].v!=b[i].v)tol++,y[tol]=b[i].v;
	    	a[b[i].p]=tol;
		}
	for(long long i=1;i<=m;i++)
	    {
	    scanf("%lld%lld",&q[i].l,&q[i].r);
	    q[i].p=i;
	    }
	sort(q+1,q+m+1,cmp2);
	for(long long i=1;i<=m;i++)
	    {
		long long l=q[i].l,r=q[i].r;
		if((l-1)/k==(r-1)/k)
	       {
	       	ms=0;
	       	for(long long j=l;j<=r;j++)
	       	    {
	       	    	tp[a[j]]++;
	                if(tp[a[j]]*y[a[j]]>ms)ms=tp[a[j]]*y[a[j]];
			    }
	       	ans[q[i].p]=ms;	       
         	for(long long j=l;j<=r;j++)tp[a[j]]--;
         	if((l-1)/k!=(q[i-1].l-1)/k||i==1)flag=1;
	       	pl=l,pr=r;
		   }
		else if((l-1)/k!=(q[i-1].l-1)/k||i==1||flag)
		   {
		   for(long long j=1;j<=n;j++)cnt[j]=0;
		   mx=0,flag=0;
		   for(long long j=((l-1)/k+1)*k+1;j<=r;j++)add(j);
		   mp=mx;
		   for(long long j=l;j<=((l-1)/k+1)*k;j++)add(j);
	       ans[q[i].p]=mx;
	       for(long long j=l;j<=((l-1)/k+1)*k;j++)cnt[a[j]]--;
	       pl=l,pr=r;
	       }
	    else
	       {
	       mx=mp;
	       for(long long j=pr+1;j<=r;j++)add(j);
	       mp=mx;
	       for(long long j=l;j<=((l-1)/k+1)*k;j++)add(j);
	       ans[q[i].p]=mx;
	       for(long long j=l;j<=((l-1)/k+1)*k;j++)cnt[a[j]]--;
	       pl=l,pr=r;
		   }
		}
	for(long long i=1;i<=m;i++)
	    printf("%lld\n",ans[i]);
	return 0;
}

后记

非常详细的莫队博客

听说还有二次离线莫队,可以处理更复杂的问题,可是我不会。

莫涛队长 orz

posted @   w9095  阅读(2)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
点击右上角即可分享
微信分享提示