莫队算法学习笔记(一)——普通莫队
前言
在学习莫队算法之前,我一直以为这是一个很高深的算法。(实际上,它就是一个很高深的算法)
这个算法玄学地将分块与暴力两大算法实现了二合一,从而打造出了一个时间复杂度为\(O(N\sqrt N)\)的求解多个区间询问的离线算法。
具体介绍
首先,我们以询问中\(l\)所在的区间为第一关键字,以\(r\)的位置为第二关键字进行排序。
然后,我们可以先暴力求解第一个询问,并将\(L\)指针指向第一个询问的\(l\),将\(R\)指针指向第一个询问的\(r\)。
随后,对于第\(i\)个问题,我们都可以将当前询问的区间\([l_i,r_i]\)的答案从上一次询问的区间\([l_{i-1},r_{i-1}]\)的答案推得。只要将\(L\)指针从\(l_{i-1}\)这个位置开始向\(l_i\)一点点移动,并且一边移动一边更新答案(更新答案的过程一般是先减去原先的答案,然后再加上更新后的新答案),对\(R\)指针的操作类似。
一道模板题
这样的描述毕竟不够具体,下面让我们看一道模板题:【洛谷2709】小B的询问
就拿我博客中的代码为例,我们来具体地看一下几个地方。
第一个地方:如何将读入的询问排序。
我们可以在读入序列的信息的同时对该序列进行分块:
//第34行
pos[i]=(i-1)/sqrt(n)+1;//将序列分块
然后,在读入每一个询问时,我们不仅要将每个询问区间的边界\(l\)和\(r\)存储下来,我们同时也要存储这个询问是第几个读入的:
//第35行
for(i=1;i<=Q;++i) read(q[i].l),read(q[i].r),q[i].pos=i;//存储下来每一个询问
这样一波操作之后,我们就可以对询问进行排序了:
//第26~29行
bool cmp(Query x,Query y)//排序所需的cmp()函数
{
//这里用了一个小技巧,首先判断两个询问l是否在同一个块,如果在同一个块,则r的大小要视l所在块编号的奇偶性而定
//若l所在块的编号为奇数,则r应从小到大,否则从大到小
//当然,这个小技巧在询问少的时候有可能反而会比全部从小到大的时间复杂度还大,不过如果询问少也用不到莫队
return pos[x.l]<pos[y.l]||(pos[x.l]==pos[y.l]&&(pos[x.l]&1?x.r<y.r:x.r>y.r));
}
//第36行
sort(q+1,q+Q+1,cmp);//将询问排序
第二个地方:如何将当前询问的答案从上一次询问的答案转移来。
这应该是莫队算法中比较核心的部分了。这里以\(L\)指针为例,\(R\)指针的操作也是类似的,具体可以参考我的代码。
第一种情况,若\(L<q[i].l\):
//第43行
ans-=cnt[a[L]]*cnt[a[L]],--cnt[a[L]],ans+=cnt[a[L]]*cnt[a[L]],++L;
//我们可以用cnt[]数组来存储每个数字出现的次数,用ans来存储答案。先将ans减去原先的答案(即cnt[a[L]]的平方),然后更新cnt[a[L]](将cnt[a[L]]减1),再将ans加上新的答案(更新后的cnt[a[L]]的平方),最后将L指针加1即可(这个操作类似于一个删除操作,所以是先删除,再更新指针)
第二种情况,若\(L>q[i].l\):
//第44行
--L,ans[q[i].pos]-=cnt[a[L]]*cnt[a[L]],++cnt[a[L]],ans[q[i].pos]+=cnt[a[L]]*cnt[a[L]];
//先将L指针减1,然后将ans减去原先的答案,在更新cnt[a[L]](将cnt[a[L]]加1),最后将ans加上新的答案(这个操作类似于一个增加操作,所以是先更新指针,再增加,与上面刚好相反)
复杂度分析
分析完了代码,最后,我们来分析一波时间复杂度:
对于\(L\)指针,有两种情况:①在块内移动。由于每个块的大小为\(\sqrt N\),所以移动的时间复杂度为\(O(\sqrt N)\),由于总共有\(Q\)个询问,所以总复杂度为\(O(Q\sqrt N)\)。②移动到了下一个块。由于每个块的大小为\(\sqrt N\),所以每次移动到下一个块的时间复杂度为\(O(\sqrt N)\),又因为总共有\(\sqrt N\)个块,所以这种情况下的总复杂度为\(O(N)\)。综上所述,\(L\)指针的移动复杂度大致为\(O(Q\sqrt N)\)。
对于\(R\)指针,我们可以发现,只要\(L\)指针所在的块不变,\(R\)指针指向的位置始终是单调递增(或递减)的,也就是说,对于同一个块内的\(L\)指针,\(R\)指针的移动复杂度是\(O(N)\)的。又因为总共有\(\sqrt N\)个块,所以\(R\)指针总的移动复杂度是\(O(N\sqrt N)\)的。
因此总共的复杂度为\(O((N+Q)\sqrt N)\),由于一般来说\(N=Q\),所以可以近似地把时间复杂度当做\(O(N\sqrt N)\),这应该是比较快的了。
莫队算法,就是这样一个玄学的算法,相当于一个巧妙的暴力。
例题
附注,另一道例题:【洛谷1494】[国家集训队] 小Z的袜子