莫队算法
Posted onglublog
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了莫队算法相关的知识,希望对你有一定的参考价值。
啥是莫队算法??
莫队算法其实本质就是暴力。
但是莫队算法在暴力的时候,规划好了每一次暴力的顺序,统筹安排暴力,可以有效地降低总时间。
怎么做呢?
首先看一道例题。
问题要我们判断每个区间里面是不是每个数都不一样。
假设你是一个刚学会编程的人,不会任何数据结构,你会怎么办呢?
有一种比较简单的思路是开一个桶,然后每次询问就清空桶,再把区间装进去,最后遍历一遍桶,看看有没有哪个桶里是有超过$1$个的。
Anyway,我们也可以遍历一遍桶,统计出一共的不同种类的数,如果种类数等于$r-l+1$就说明每个数都不一样。
这样我们就很接近莫队算法的思想了。
1.相邻区间转移
现在考虑,我们已经统计出了$[l,r]$,如何知道$[l,r+1]$的种类数。
我们已经有了$[l,r]$中每种数的个数,我们把$a[r+1]$加入,如果发现$cnt[a[r+1]]$是空的,我们种类数$++$,然后$cnt[a[r+1]]++$
这样子我们就能从$[l,r]$转移到$[l,r+1]$了。
这种转移是$O(1)$的,同理,剩下几个边界加加减减都很好推出来。
1 void add(int x){ 2 if(!cnt[a[x]])now++; 3 cnt[a[x]]++; 4 } 5 void del(int x){ 6 --cnt[a[x]]; 7 if(!cnt[a[x]])now--; 8 }
我们也可以通过位运算压一下位
1 void add(int x){now+=!cnt[a[x]]++;} 2 void del(int x){now-=!--cnt[a[x]];}
这个代码可以直接插在主函数里面,可以加快不少的时间。
2.询问排序
通过区间转移,我们可以很方便地用上一个询问求出的数据去求出下一个询问了。
但是,如果两个询问之间距离很大,程序还是$O(nm)$的。
我们可以离线回答询问,这样子就可以通过统筹回答,降低时间了。
一个比较容易想到的方法是按照左端点的大小排序。
但是这样子只要区间的大小交替变换就能卡掉了。
比如说下面这组数据
$[1,1000],[2,3],[3,1000],[4,4]$
明显按照左端点排序是行不通的。
我们可以按照块给左端点排序,左端点在同一个块内时右端点升序。
只要分块合理,在块内移动所需要的时间是很小的,也就是说我们给左端点一定的容错性。
在一定的范围内,左端点可以来回移动,这样子既保证了左端点的复杂度,右端点的复杂度也不会上天。
在随机数据下,块的大小为$frac{n}{m imes frac{2}{2}}$时,时间复杂度比较优秀。
我们排序之后跑暴力就十分快了。
完整代码:
1 #include <bits/stdc++.h> 2 using namespace std; 3 const int N=5e5+1009; 4 struct Q{ 5 int l,r,id; 6 Q(int aa=0,int bb=0,int cc=0){l=aa;r=bb;id=cc;} 7 }q[N]; 8 int read(){ 9 char c;int num,f=1; 10 while(c=getchar(),!isdigit(c))if(c==‘-‘)f=-1;num=c-‘0‘; 11 while(c=getchar(), isdigit(c))num=num*10+c-‘0‘; 12 return f*num; 13 } 14 int n,m,a[N],block,ans[N]; 15 int cnt[N],now=0; 16 bool cmp(Q a,Q b){ 17 return (a.l/block==b.l/block)?a.r<b.r:a.l/block<b.l/block; 18 } 19 void add(int x){now+=!cnt[a[x]]++;} 20 void del(int x){now-=!--cnt[a[x]];} 21 int main() 22 { 23 n=read();m=read(); 24 for(int i=1;i<=n;i++)a[i]=read(); 25 for(int j=1;j<=m;j++){ 26 q[j].id=j; 27 q[j].l=read(); 28 q[j].r=read(); 29 } 30 block=n/sqrt(m*2/3); 31 sort(q+1,q+1+m,cmp); 32 int nl=1,nr=1; 33 add(1); 34 for(int i=1;i<=m;i++){ 35 int l=q[i].l,r=q[i].r; 36 while(nr<r)add(++nr); 37 while(nr>r)del(nr--); 38 while(nl<l)del(nl++); 39 while(nl>l)add(--nl); 40 ans[q[i].id]=(now==(r-l+1)); 41 } 42 for(int i=1;i<=m;i++) 43 printf("%s ",ans[i]?"Yes":"No"); 44 return 0; 45 }
这里有一个邪门优化,可以让复杂度除2。
我们在按左端点分块排序的基础上,对奇偶编号的块交替升降序对右端点排序。
也就是说奇数的时候右端点升序,偶数的时候右端点降序。
这样可以玄学降低时间复杂度。
为什么可以这么做呢,假设我们一开始升序,排序完之后,右端点在最右边。如果新的块内还是升序的话,我们就要先从右跑到左,然后再跑回去,这样是很慢的。
为何不从右跑的左的时候顺便统计了呢?
这样子,原来要跑两趟,现在只需要一趟就可以了,对时间的优化其实是很大的。
时间复杂度为$O(nsqrt{m})$
带修莫队和树上莫队留坑待补。。
以上是关于莫队算法的主要内容,如果未能解决你的问题,请参考以下文章