简介
这一系列文章会介绍最基础的几种莫队的写法与例题(一些看不懂的地方也可以在评论里问我)。包括的普通莫队,带修莫队,回滚莫队。莫队,一种优雅的暴力。之所以称之为暴力,是因为它的所有操作都是暴力转移的。那为什么莫队可以降低时间复杂度呢,是通过排序来使转移更加高效。因为很多区间之间都有互相包含的部分。如果全部重新扫的话,很显然会浪费许多时间。莫队就是来帮助我们进行高效转移的。
而莫队的转移都是依靠一定的排序标准的。将每种莫队的询问区间进行一定的标准排序后,就可以按照排序完的顺序,每次在两个相邻的区间内暴力转移即可。
不同莫队写法因为排序算法的不同,时间也有差异。其实理论上莫队算法的最优解法是将所有点放到图上,建立一颗曼哈顿距离最小生成树。因为这样,每次两个区间的差异就是最小的。但是因为实现麻烦,并且常数大,所以排序算法已经够满足我们的需求了。
各种莫队的算法与实现
普通莫队
算法概括
普通莫队,一般就是有多组询问区间,让你求出这段区间里的一些信息。
算法思路
很明显,我们需要先存下所有询问区间,然后需要对这些区间进行排序(而其实莫队的核心差别也就是该如何排序)。那我们思考如何排序才能降低时间复杂度。
这时就要介绍到莫队的核心也是本质思想,分块(当然,与真正的分块还有一定的区别)。就是将一定范围内的点放在一个块内。而显然,在这个块内每个区间暴力转移的时间复杂度就会变低(从原来每次转移O(n)变成了每次转移O(块的大小))。
那我们就可以假设我们分了一些块,那我们就可以把所有区间中左端点所在块相同的区间放在一起,那此时,他们左端点转移的时间就成功降低了。那么有端点该怎么办呢?很显然,我们只要求了左端点所在的块相同。那么在相同块内左端点所对应的右端点就可以随便排序。那么显然拍成升序就是最优的了。因为这样一个块内所有区间查询的时间和也才 O ( n ) O(n) O(n) 级别。
那我们该如何选择块长呢?这也是莫队最重要的一个点。虽然考场上最常见的做法是造一组极限数据,然后对着极限数据调块长就可以了。但是知道块长到底是如何推出来的,也能帮助我们更好的理解为什么要这么选块长,让我们更好的理解各种莫队的差异。
首先,设块长为 S,询问次数为 q ,区间长度为 n 。那么显然,同一块内左区间转移的时间复杂度是 O ( q S ) O(qS) O(qS) ,因为在同一个块内的左端点单次转移为时间复杂度显然就是块长。而右端转移的时间复杂度为 O ( n n S ) O(n\frac{n}{S}) O(nSn) 。因为左端点在同一块内的区间,右端点的转移时间为 O ( n ) O(n) O(n) ,而一共有 n S \frac{n}{S} Sn 个块。
那我们该如何让这两个操作的复杂度综合最小呢?知道时间复杂度分析的伙伴们应该知道,在忽略常数的情况下,复杂度小的部分可以直接省略(因为两者通常会有几十上百倍的差距,小的部分对时间的影响不大)。而显然,一般情况下,两个区间不可能同时变小(毕竟哪有这种鱼和熊掌可以兼得的好事呢当然,可能有这种情况,但至少我没有遇见过)。两个部分肯定一大一小,所以直接使两个部分相等就行了。这时我们就可以得到式子:
q
S
=
n
n
S
qS = n\frac{n}{S}
qS=nSn
显然,我们可以经过一同转移(公式可能用的不严谨,但主要是为了大家能看懂是如何求出块长的):
q S 2 = n 2 → S 2 = n 2 q → S = n 2 q qS^2 = n^2 \rightarrow S^2=\frac{n^2}{q} \rightarrow S= \sqrt{\frac{n^2}{q}} qS2=n2→S2=qn2→S=qn2
而在大部分的题目中,n与q是同阶的,所以这就是你为什么在很多题目中看到别人将块长设为
n
\sqrt{n}
n 的原因。这样,最后的时间复杂度就是
O
(
q
n
2
q
)
O(q\sqrt{\frac{n^2}{q}})
O(qqn2) 。关于具体代码的实现与细节,我全部写在代码里了。可以按照程序运行的顺序从头到尾读一遍自认为我的注释还是写的比较清晰的,实在看不懂可以在评论里问我。
具体代码
#include<bits/stdc++.h>
using namespace std;
const int N=50011;
int n,m,kuai[N],c[N],sum,use,a[N];
struct node{
int l,r,id;
}q[N<<1];
struct node1{
int a1,a2;
}ans[N];
bool cmp(node aa,node bb){
//把左端点在同一块内的放在一起,在同一块内的按右端点排序
if(kuai[aa.l]==kuai[bb.l])return aa.r<bb.r;
return aa.l<bb.l;
}
void add(int k,int now){
c[a[k]]++;
sum+=abs(k-now);
use+=c[a[k]]-1;
}
void cut(int k,int now){
c[a[k]]--;
sum-=abs(k-now);
use-=c[a[k]];
}
int gcd(int a,int b){
if(a<b)swap(a,b);
if(b==0)return a;
return gcd(b,a%b);
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=m;i++){
cin>>q[i].l>>q[i].r;
q[i].id=i;
}
int len=sqrt(n);//计算块长
for(int i=1;i<=len;i++)kuai[i]=1;
for(int i=len+1;i<=n;i++){
kuai[i]=kuai[i-len]+1;//kuai[i]表示i这个点是哪个块的
}
//我是用的递推求块长,会比直接计算要快一点
sort(q+1,q+m+1,cmp);
int lnow=q[1].l,rnow=q[1].l-1;
//莫队的初始化,记得右端点要-1,因为l,r表示已经取了l~r之间的数。
//l,r赋成同样的值的话是去不到 r 的。
for(int i=1;i<=m;i++){//相邻的两组询问直接暴力转移就可以了
while(rnow<q[i].r)add(++rnow,lnow);//移动左右端点
while(lnow<q[i].l)cut(lnow++,rnow);
while(lnow>q[i].l)add(--lnow,rnow);
while(rnow>q[i].r)cut(rnow--,lnow);
int gcd_bei;
if(use==0)ans[q[i].id].a1=0,ans[q[i].id].a2=1;//计算答案,每到题目都不一样
else{
gcd_bei=gcd(use,sum);
ans[q[i].id].a1=use/gcd_bei;
ans[q[i].id].a2=sum/gcd_bei;
}
}
for(int i=1;i<=m;i++){
cout<<ans[i].a1<<"/"<<ans[i].a2<<endl;
}
}
带修莫队
写在下一篇博客