莫队算法详解系列(一) 普通莫队
先从一道题说起吧。
HYSBZOJ 2038 小Z的袜子(hose)
题目大意
给定一个区间 [ L , R ] [L,R] [L,R],输出该区间内抽出颜色相同的袜子的概率。
分析
直接暴力查找,复杂度为 O ( n 2 ) O(n^2) O(n2),在题目条件下显然超时。
考虑推一下公式:设 T i T_i Ti为第 i i i种颜色在 [ L , R ] [L,R] [L,R]中的出现的次数。
则我们易得: P ( 抽 到 相 同 颜 色 ) = ∑ T i ( T i − 1 ) ( R − L + 1 ) ( R − L ) = ∑ T i 2 − ∑ T i ( R − L + 1 ) ( R − L ) P(抽到相同颜色)=\frac{\sum{T_i(T_i-1)}}{(R-L+1)(R-L)}\\=\frac{\sum{{T_i}^2}-\sum{T_i}}{(R-L+1)(R-L)} P(抽到相同颜色)=(R−L+1)(R−L)∑Ti(Ti−1)=(R−L+1)(R−L)∑Ti2−∑Ti
T i T_i Ti好维护,可 ∑ T i 2 \sum {T_i}^2 ∑Ti2直接维护就有点麻烦了。
那我们调整一下算法顺序呢?
利用一个变量记录一下区间 [ l , r ] [l,r] [l,r]中 的 ∑ T i 2 的\sum{T_i}^2 的∑Ti2,显然从 [ l , r ] [l,r] [l,r]到 [ l + 1 , r ] , [ l − 1 , r ] , [ l , r + 1 ] , [ l , r − 1 ] [l+1,r],[l-1,r],[l,r+1],[l,r-1] [l+1,r],[l−1,r],[l,r+1],[l,r−1]的更新答案的复杂度为 O ( 1 ) O(1) O(1)。
我们考虑如下算法:
显然这个算法也是 O ( N ) O(N) O(N)的。
考虑调整查询的顺序,我们按起点将所有的查询分为 N \sqrt N N块,每块的大小为 N N N\sqrt N NN。所有左端点落在第一块的查询将会被先处理,而不管其右端点在哪个块。且每个块中,按右端点的升序进行排列。
这样我们就得到了莫队算法。
时间复杂度的讨论
为啥块的大小要用 N \sqrt N N?
我们在计算时运用了两个指针 l , r l,r l,r,设每个块的大小为 M M M。
对于指针 l l l,若两个询问不同块,那么它最多只需 O ( 2 M ) O(2M) O(2M)即可到达,故总时间为 O ( N M ) O(NM) O(NM)。
对于指针 r r r,由于它几乎没有顺序,所以在计算一个块答案时,它需要移动最多 N N N次,又由于有 O ( N / M ) O(N/M) O(N/M)个块,所以总复杂度为 O ( N 2 M ) O(\frac{N^2}{M}) O(MN2)。
综上,总时间复杂度为 O ( N M + N 2 M ) O(NM+\frac{N^2}{M}) O(NM+MN2),根据基本不等式,当 M = N M=\sqrt N M=N时,莫队取得最低时间复杂度 O ( N N ) O(N\sqrt N) O(NN)。所以自然块的大小是 N \sqrt N N的了。
参考代码
#include<cmath>
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
const int Maxn=50000;
int N,Q,A[Maxn+5];
int Block;
struct Query {
int l,r,id;
ll a,b;
};
bool cmp_query(Query lhs,Query rhs){return (lhs.l/Block==rhs.l/Block?lhs.r<rhs.r:lhs.l/Block<rhs.l/Block);}
bool cmp_getans(Query lhs,Query rhs){return lhs.id<rhs.id;}
ll GCD(ll x,ll y) {
if(y==0)return x;
return GCD(y,x%y);
}
Query q[Maxn+5];
int sum[Maxn+5];
ll ans;
void add(int pos) {
ans-=(1LL*sum[A[pos]]*sum[A[pos]]);
sum[A[pos]]++;
ans+=(1LL*sum[A[pos]]*sum[A[pos]]);
}
void del(int pos) {
ans-=(1LL*sum[A[pos]]*sum[A[pos]]);
sum[A[pos]]--;
ans+=(1LL*sum[A[pos]]*sum[A[pos]]);
}
int main() {
#ifdef LOACL
freopen("in.txt","r",stdin);
freopen("out.txt","w",stdout);
#endif
scanf("%d %d",&N,&Q);
Block=sqrt(N);
for(int i=1;i<=N;i++)
scanf("%d",&A[i]);
for(int i=1;i<=Q;i++) {
scanf("%d %d",&q[i].l,&q[i].r);
q[i].id=i;
}
sort(q+1,q+Q+1,cmp_query);
int l=0,r=0;
for(int i=1;i<=Q;i++) {
while(l<q[i].l)
del(l),l++;
while(l>q[i].l)
l--,add(l);
while(r<=q[i].r)
add(r),r++;
while(r>q[i].r+1)
r--,del(r);
q[i].a=ans-(q[i].r-q[i].l+1);
q[i].b=1LL*(q[i].r-q[i].l+1)*(q[i].r-q[i].l);
ll tmp=GCD(q[i].a,q[i].b);
q[i].a/=tmp,q[i].b/=tmp;
}
sort(q+1,q+Q+1,cmp_getans);
for(int i=1;i<=Q;i++)
printf("%lld/%lld\n",q[i].a,q[i].b);
return 0;
}
总结
莫队对问题的要求是非常严格的,具体来说问题应满足如下条件:
- 离线查询;
- 时间复杂度较高时能够承受;
- 推出相邻情况的复杂度为常数;
- 由于利用分块,若分块爆了,它也会跟着爆。