//https://img2018.cnblogs.com/blog/1646268/201908/1646268-20190806114008215-138720377.jpg

莫队算法

莫队

不是提莫队长。

普通莫队

莫队算法是由莫涛发明的算法,所以称为莫队算法。
莫队算法可以说是把暴力和分块融合在一起的一种算法,主要可以解决一些不强制在线的操作;
主要的思想就是通过挪动区间指针来减少时间复杂度,这个需要把每一次询问的区间给存起来,然后按照“如果区间的左端点在一个块里就按右端点从小到大排序,如果不在同一块就按左端点的大小排序”的规则,将询问排序,然后进行区间的挪动可以大大减少时间复杂度,然后把每一次得到的答案给离线存放到数组里,最后一起输出即可。

比如这道例题

P2709 小B的询问

首先我们拿到题面,可以看到询问操作是 \(l\)\(r\) 之间的所有数的出现次数的平方的和,不难发现如果要是删掉一个数的话他的之就会减少 \(n^{2}-(n-1)^{2}\) 也就是 \(2n-1\)\(n\) 是当前数在此区间内的出现次数,如果要是加入某一个数的话,那么增加的就是 \((n+1)^{2}-n^{2}\) 也就是 \(2n+1\),然后就是普通莫队啦。

#include<bits/stdc++.h>
#define int long long
#define endl '\n'
#define N 100010
using namespace std;
int ans[N],a[N],b[N],n,m,k,c,kc;
struct modui{int l,r,id;}e[N];
inline int read(){int x=0,fh=1;char ch=getchar();while(!isdigit(ch)){if(ch=='-') fh=-1;ch=getchar();}while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}return x*fh;}
inline int cmp(modui a,modui b)
{
	if((a.l-1)/kc==(b.l-1)/kc)return a.r<b.r;
	return a.l<b.l;
}
inline void add(int x)
{
	c+=2*b[x]+1;
	b[x]++;
}
inline void dele(int x)
{
	c-=2*b[x]-1;
	b[x]--;
}
signed main()
{
	int L,R,ans1=1,ans2=0;
	n=read(),m=read(),k=read();
	kc=sqrt(n);
	for(int i=1;i<=n;i++)
	  a[i]=read();
	for(int i=1;i<=m;i++)
	{
		e[i].l=read();
		e[i].r=read();
		e[i].id=i;
	}
	sort(e+1,e+m+1,cmp);
	for(int i=1;i<=m;i++)
	{
		L=e[i].l,R=e[i].r;
		while(ans1>L)ans1--,add(a[ans1]);
		while(ans2<R)ans2++,add(a[ans2]);
		while(ans1<L)dele(a[ans1]),ans1++;
		while(ans2>R)dele(a[ans2]),ans2--;
		ans[e[i].id]=c;
	}
	for(int i=1;i<=m;i++)
		cout<<ans[i]<<endl;
	return 0;
}

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

题目询问对于一个区间内取两个数相同的概率,那么我们可以知道,这种取数是属于那种取出不放回的,所以我们的分母也就是取出的数的情况总数就是 \(\frac{(r-l+1)(r-l)}{2}\) ,那么我们就可以再去算取出的两个数可能是 \(x\) 的情况数,此时我们设 \(n\)\(x\) 在此区间内出现的次数,那么我们也很容易就得出 \(x\) 对于此次的询问的贡献就是 \(\frac{n\times (n-1)}{2}\),删除掉一个 \(x\) 就相当于在分子上减去 \(\frac{n\times (n-1)}{2}-\frac{(n-1)(n-2)}{2}\),化简完得 \(n-1\),加上一个 \(x\) 就相当于在分子上加 \(\frac{(n+1)\times n}{2}-\frac{n\times (n-1)}{2}\),化简完得 \(n\)

#include<bits/stdc++.h>
#define int long long
#define N 100100
using namespace std;
int n,m,kc,a[N],ans[N][2],b[N],fz;
struct sb{int l,r,id;}e[N];
inline int read(){int x=0,fh=1;char ch=getchar();while(!isdigit(ch)){if(ch=='-') fh=-1;ch=getchar();}while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}return x*fh;}
inline int cmp(sb a,sb b)
{
	if((a.l-1)/kc==(b.l-1)/kc)return a.r<b.r;
	else return a.l<b.l;
}
inline void dele(int x)
{
	fz-=b[x]-1;
	b[x]--;
}
inline void add(int x)
{
	fz+=b[x];
	b[x]++;
}
signed main()
{
	int L,R,ans1=1,ans2=0;
	n=read();m=read();
	kc=sqrt(n);
	for(int i=1;i<=n;i++)
	  a[i]=read();
	for(int i=1;i<=m;i++)
	{
		e[i].l=read();
		e[i].r=read();
		e[i].id=i;
	}
	sort(e+1,e+m+1,cmp);
	for(int i=1;i<=m;i++)
	{
		L=e[i].l;R=e[i].r;
		if(L==R)
		{
			ans[e[i].id][0]=0;
			ans[e[i].id][1]=1;
			continue;
		}
		while(ans1>L)ans1--,add(a[ans1]);
		while(ans2<R)ans2++,add(a[ans2]);
		while(ans1<L)dele(a[ans1]),ans1++;
		while(ans2>R)dele(a[ans2]),ans2--;
		int c=R-L+1,fm=c*(c-1)/2;
		int g=__gcd(fz,fm);
		ans[e[i].id][0]=fz/g;
		ans[e[i].id][1]=fm/g;
	}
	for(int i=1;i<=m;i++)
		cout<<ans[i][0]<<"/"<<ans[i][1]<<endl;
	return 0;
}

带修莫队

首先我们要知道,普通的莫队算法是不资瓷修改操作的,不过后人对莫队算法加以改进发明了资瓷修改的莫队算法。

在进行修改操作的时候,修改操作是会对答案产生影响的(废话),那么我们如何避免修改操作带来的影响呢?首先我们需要把查询操作和修改操作分别记录下来。在记录查询操作的时候,需要增加一个变量来记录离本次查询最近的修改的位置,然后套上莫队的板子,与普通莫队不一样的是,你需要用一个变量记录当前已经进行了几次修改。对于查询操作,如果当前改的比本次查询需要改的少,就改过去,反之如果改多了就改回来。

比如,我们现在已经进行了3次修改,本次查询是在第5次修改之后,那我们就执行第4,5次修改,这样就可以避免修改操作对答案产生的影响了。

同时我们需要对排序的规则进行一下修改:如果左端点在同一区块且右端点在同一区块,则按时间排序;如果左端点在同一区块而右端点不在同一区块,则按右端点排序;如果左端点不在同一区块,则按左端点排序。

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

#include<bits/stdc++.h>
#define int long long
#define endl '\n'
#define N 1001000
using namespace std;
struct node{int l,r,t,id;}e1[N];
struct Node{int id,k;}e2[N];
int n,m,kc,now,a[N],cnt[N],b[N],ans[N],cnt1,cnt2;
inline int read(){int x=0,f=1;char ch=getchar();while(!isdigit(ch)){f=ch!='-';ch=getchar();}while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return f?x:-x;}
inline int cmp(node a,node b)
{
	if(a.l/kc==b.l/kc)
	{
		if(a.r/kc==b.r/kc)return a.t<b.t;
		else return a.r<b.r;
	}
	else return a.l<b.l;
}
inline void cxk(int l,int r,int x)
{
	int xx=e2[x].id;
	int &kk=e2[x].k;
	if(xx>=l&&xx<=r)
	{
		now-=! --cnt[a[xx]];
		now+=! cnt[kk]++;
	}
	swap(a[xx],kk);
}
signed main()
{
	n=read();
	m=read();
	kc=pow(n,0.666);
	for(int i=1;i<=n;i++)
	  a[i]=read();
	for(int i=1;i<=m;i++)
	{
		char op;
		int l,r;
		cin>>op;
		if(op=='Q')
		{
			e1[++cnt1].l=read();
			e1[cnt1].r=read();
			e1[cnt1].t=cnt2;
			e1[cnt1].id=cnt1;
		}
		else
		{
			e2[++cnt2].id=read();
			e2[cnt2].k=read();
		}
	}
	sort(e1+1,e1+cnt1+1,cmp);
	int L,R,T,l=1,r=0,t=0;now=0;
	for(int i=1;i<=n;i++)
	{
		L=e1[i].l;
		R=e1[i].r;
		T=e1[i].t;
		while(l<L)now-=! --cnt[a[l++]];
		while(l>L)now+=! cnt[a[--l]]++;
		while(r<R)now+=! cnt[a[++r]]++;
		while(r>R)now-=! --cnt[a[r--]];
		while(t<T)cxk(L,R,++t);
		while(t>T)cxk(L,R,t--);
		ans[e1[i].id]=now;
	}
	for(int i=1;i<=cnt1;i++)
	  cout<<ans[i]<<endl;
	return 0;
}

回滚莫队

回滚莫队这个东西,一般是在加入的操作很好搞,但删除的时候很难搞的时候用的,比如问你区间内的最值问题。

当你在处理询问的时候,我们都知道当左端点处于同一块的时候,右端点是从小到大单调递增的,所以我们想到,可以先把左端点设为当前块右端点+1,然后右端点设为当前块右端点,这样只要你想查询,就必须向外扩展然后进行加的操作,我们知道右端点单调递增了,所以我们可以开一个变量存上一次的答案,然后下一次直接调用,每一次处理完一个询问就恢复左端点,然后恢复答案的值,然后下一次开始加。

对于lr在同一区间内的情况,直接暴力求答案。

板子

#include<bits/stdc++.h>
#define int long long
#define N 1000100
using namespace std;
int n,m,kc,bl[N],ans[N],Ais,last,cnt[N],cnt1[N];
int len,a[N],v[N],cao[N],block;
struct sb{int l,r,id;}e[N];
inline int read(){int x=0,f=1;char ch=getchar();while(!isdigit(ch)){f=ch!='-';ch=getchar();}while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return f?x:-x;}
inline int cmp(sb a,sb b){if(bl[a.l]==bl[b.l])return a.r<b.r;return a.l<b.l;}
inline void add(int x){++cnt[v[x]];Ais=max(Ais,cnt[v[x]]*a[x]);}//加贡献的时候计算最大值 
inline void del(int x){--cnt[v[x]];}//减操作 
inline int slove(int l,int r)
{
	int maxn=0;
	for(int i=l;i<=r;i++)cnt1[v[i]]=0;//清空cnt1数组 
	for(int i=l;i<=r;i++)//枚举每一个区间找最大值 
	{
		++cnt1[v[i]];
		maxn=max(maxn,cnt1[v[i]]*a[i]);
	}
	return maxn;//返回答案 
}
signed main()
{
	n=read();m=read();
	kc=sqrt(n);
	for(int i=1;i<=n;i++)
	  a[i]=read(),cao[i]=a[i],bl[i]=(i-1)/kc+1;//计算块,存a数组 
	block=bl[n];//块的数量 
	sort(cao+1,cao+n+1);//将cao从小到大排序 
	len=unique(cao+1,cao+n+1)-cao-1;//去重取出长度 
	for(int i=1;i<=n;i++)
	  v[i]=lower_bound(cao+1,cao+len+1,a[i])-cao;//计算当前ai在cao中去重后的位置 
	for(int i=1;i<=m;i++)//输入询问的信息 
	  e[i].l=read(),e[i].r=read(),e[i].id=i;
	sort(e+1,e+m+1,cmp);//将询问排序 
	int p=1;//当前询问的编号 
	for(int i=1;i<=block;i++)//枚举每一个块 
	{
		Ais=0;last=0;//ans和last清空 
		for(int j=1;j<=n;j++)cnt[j]=0;//清空cnt数组 
		int t=min(kc*i,n);//极限边界 
		int l=t+1,r=t;//左边界一开始最大,r一开始等于当前块右端点 
		for(;bl[e[p].l]==i;p++)//如果当前询问的左端点是在当前块里就询问的编号不断累加 
		{
			if(bl[e[p].l]==bl[e[p].r])//左右端点在同一块里 
			{
				ans[e[p].id]=slove(e[p].l,e[p].r);//直接暴力求值 
				continue;//跳过 
			}
			while(r<e[p].r)add(++r);//如果要是右边界小就加 
			last=Ais;//last记录当前的答案,l为右端点的答案 
			while(l>e[p].l)add(--l);//如果要是当前点的左端点的大于询问的左端点,直接加 
			ans[e[p].id]=Ais;//得到答案 
			while(l<=t)del(l++);//恢复左端点 
			Ais=last;//撤回答案 
		}
	}
	for(int i=1;i<=m;i++)
	  cout<<ans[i]<<endl;//输出答案 
	return 0;
}
posted @ 2023-01-18 09:09  北烛青澜  阅读(65)  评论(0编辑  收藏  举报