题面传送门
莫队,由原国家集训队队员莫涛同学提出的一种优雅的暴力,在分块的基础上以
O
(
n
)
O(\sqrt{n})
O(n)的均摊复杂度维护大量的区间信息(这里的大量,意为每个节点有很多信息,并非多次询问)。因其思路清晰,代码简单,变化多样,维护区间信息多样化,成为数据结构题的骗分神器,也成为了各大数据结构比赛的一大考点
莫队在分块的思维上用暴力的手段维护区间信息。
例如本题,暴力用
O
(
n
)
O(n)
O(n)的复杂度循环枚举答案。
我们可以换个思路暴力,设上一个的区间为
l
1
,
r
1
l_1,r_1
l1,r1,设现在要维护的区间为
l
2
,
r
2
l_2,r_2
l2,r2,则让
l
1
l_1
l1向
l
2
l_2
l2移,
r
1
r_1
r1向
r
2
r_2
r2移,在移的过程中用上一次的答案以及每移一步更新的答案生成出新的答案,时间复杂度
O
(
n
)
O(n)
O(n)
很明显,这种方法的时间复杂度与区间之间的相对位置有关,而我们可以人为地改变这个顺序,所以说莫队是一种离线算法。
那排序的那个
c
m
p
cmp
cmp函数里面写什么就非常重要了,尝试一下,按左端点排序?那总共左端点的时间复杂度为
O
(
n
)
O(n)
O(n),而右端点可以达到
O
(
n
m
)
O(nm)
O(nm),用右端点排序也是一样。我们要有一个方法让左端点和右端点距离下一个端点的最大值尽可能地最小
此时莫队的精髓就在里面了,莫队算法把整个数列分为
k
k
k块,左端点如果不在一个块内按左端点所在块排,左端点如果在同一个块里按右端点排序
我们可以来计算一下这样的话的时间复杂度是多少,总共有
k
k
k块,对于每一块,一次移动左端点最多移动
n
k
\frac{n}{k}
kn次,那么全部算下来左端点最多移动
n
m
k
\frac{nm}{k}
knm次,右端点整块最多移动
n
n
n次,全部下来右端点最多移动
k
n
kn
kn次,时间复杂度为
O
(
n
m
k
+
k
n
)
O(\frac{nm}{k}+kn)
O(knm+kn),此时
k
k
k取
n
\sqrt{n}
n为最小值,全部复杂度为
O
(
(
n
+
m
)
n
)
O((n+m)\sqrt{n})
O((n+m)n)
说到底,莫队算法和其他维护区间信息的题目一样,难就难在如何维护区间信息,例如这一题,当
c
i
+
1
c_i+1
ci+1时,答案怎么维护?很明显是加上
2
c
i
+
1
2c_i+1
2ci+1,减也是减去
2
c
i
−
1
2c_i-1
2ci−1
代码实现:
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
int n,m,k,ks,f[100039],s[100039],q[100039],tot,pus,a[100039],l,r,ans[100039];
struct yyy{
int x,y,num;
}sf[100039];
inline bool cmp(yyy x,yyy y){
if(s[x.x]==s[y.x]) return x.y<y.y;
return x.x<y.x;
}
int main(){
register int i,j;
scanf("%d%d%d",&n,&m,&k);
ks=sqrt(n);
for(i=1;i<=n;i++)scanf("%d",&a[i]);
for(i=1;i<=ks;i++) s[i]=1;
for(i=ks+1;i<=n;i++) s[i]=s[i-ks]+1;
for(i=1;i<=m;i++){
scanf("%d%d",&sf[i].x,&sf[i].y);
sf[i].num=i;
}
sort(sf+1,sf+m+1,cmp);
l=sf[1].x;
r=sf[1].y;
for(i=sf[1].x;i<=sf[1].y;i++){
tot+=f[a[i]]*2+1;
f[a[i]]++;
}
ans[sf[1].num]=tot;
for(i=2;i<=m;i++){
while(sf[i].x<l) l--,tot+=f[a[l]]*2+1,f[a[l]]++;
while(sf[i].x>l) tot-=f[a[l]]*2-1,f[a[l]]--,l++;
while(sf[i].y<r) tot-=f[a[r]]*2-1,f[a[r]]--,r--;
while(sf[i].y>r) r++,tot+=f[a[r]]*2+1,f[a[r]]++;
ans[sf[i].num]=tot;
}
for(i=1;i<=m;i++) printf("%d\n",ans[i]);
}