莫队算法

啥是莫队算法??

莫队算法其实本质就是暴力。

但是莫队算法在暴力的时候,规划好了每一次暴力的顺序,统筹安排暴力,可以有效地降低总时间。

怎么做呢?

首先看一道例题。

P3901 数列找不同

问题要我们判断每个区间里面是不是每个数都不一样。

假设你是一个刚学会编程的人,不会任何数据结构,你会怎么办呢?

有一种比较简单的思路是开一个桶,然后每次询问就清空桶,再把区间装进去,最后遍历一遍桶,看看有没有哪个桶里是有超过$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 }
View Code

我们也可以通过位运算压一下位

1 void add(int x){now+=!cnt[a[x]]++;}
2 void del(int x){now-=!--cnt[a[x]];}
View Code

这个代码可以直接插在主函数里面,可以加快不少的时间。

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 }
View Code


这里有一个邪门优化,可以让复杂度除2。

我们在按左端点分块排序的基础上,对奇偶编号的块交替升降序对右端点排序。

也就是说奇数的时候右端点升序,偶数的时候右端点降序。

这样可以玄学降低时间复杂度。

为什么可以这么做呢,假设我们一开始升序,排序完之后,右端点在最右边。如果新的块内还是升序的话,我们就要先从右跑到左,然后再跑回去,这样是很慢的。

为何不从右跑的左的时候顺便统计了呢?

这样子,原来要跑两趟,现在只需要一趟就可以了,对时间的优化其实是很大的。

时间复杂度为$O(nsqrt{m})$

带修莫队和树上莫队留坑待补。。

原文地址:https://www.cnblogs.com/onglublog/p/10158669.html