📅  最后修改于: 2023-12-03 15:39:01.419000             🧑  作者: Mango
MO算法是用来解决带修改的查询问题的,比如查询一个序列中区间内出现次数最多的元素,或者求出一个序列中不同数的个数等等。它的基本思路是把所有询问离线下来,按照一定的顺序排序,然后把它们放到一个个的块中去,每个块中的询问不会对块之外的元素造成影响。这样我们就可以用类似根号分治的方法,对每个块依次处理,并对每个块上的询问进行回答。
这里我们介绍一种常用的MO算法的实现,用来解决求一个序列的子数组中小于或等于给定数字的元素个数的问题。
我们把询问按照其左端点所处的块的编号进行排序,并且对于属于同一块的询问按照右端点的大小排序。 由于我们直接对元素个数进行计数,所以并不需要对询问进行离散化处理。
为了方便后续操作,我们使用两个指针$L$和$R$分别指向当前块的左右端点,使用一个桶$cnt$来记录当前块的每个元素出现的次数。
当我们遍历到一条询问时,首先把$L$和$R$移动到询问的左右端点。并且处理出$L$和$R$所在的块,以及当前询问的左右端点分别在块内的位置。
接下来我们需要让$[L,R]$中的元素按照出现次数进行计数。这里有一个小技巧,我们并不需要重新计算从$L$到$R$之间的元素的出现次数,而只需要调整原本的计数结果。这样的调整可以在$O(\sqrt{n})$ 的时间内完成。
最后,我们可以在$cnt$桶中查找小于或等于给定数字的元素个数,这个查询操作的复杂度是$O(\sqrt{n})$的。
总的时间复杂度是$O(q\sqrt{n})$的。
const int N=1e5+10;
const int M=350; //块长
struct Query{
int l,r,id,pos;
}q[N];
int n,m;
int a[N],cnt[N],ans[N];
int block;
bool cmp(Query q1,Query q2){
if(q1.pos!=q2.pos) return q1.pos<q2.pos;
if(q1.pos&1) return q1.r<q2.r;
return q1.r>q2.r;
}
void add(int x,int& res){ //计数+1
res-=(cnt[a[x]]==x);
cnt[a[x]]++;
res+=(cnt[a[x]]==x);
}
void del(int x,int& res){ //计数-1
res-=(cnt[a[x]]==x);
cnt[a[x]]--;
res+=(cnt[a[x]]==x);
}
void MO_algorithm(){
sort(q+1,q+1+m,cmp);
int L=1,R=0,res=0;
for(int i=1;i<=m;i++){
int ql=q[i].l,qr=q[i].r;
while(L<ql) del(L++,res);
while(L>ql) add(--L,res);
while(R<qr) add(++R,res);
while(R>qr) del(R--,res);
ans[q[i].id]=res;
}
}
int main(){
scanf("%d%d",&n,&m);
block=sqrt(n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=m;i++){
scanf("%d%d",&q[i].l,&q[i].r);
q[i].pos=(q[i].l-1)/block+1;
q[i].id=i;
}
MO_algorithm();
for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
return 0;
}
代码解释:
定义了结构体Query,用来存储每一个询问的信息。字段$l$和$r$分别表示该询问的左右端点,$id$表示该询问在输入顺序中的编号,$pos$表示该询问的左端点所在的块的编号。
实现了两个函数 $add$ 和 $del$,用来计算$[L,R]$中元素的出现次数。这两个函数分别表示计数增加一次和计数减少一次的操作,同时也要记录着从元素个数发生变化后,小于数个数的取值是否发生变化,最后要调整取值的变化。
实现了MO_algorithm函数,主要是对每一个询问进行处理,并且计算出每一个询问的答案。
最后就是读入数据,对询问进行排序,并且调用MO_algorithm函数进行处理,最后输出所有询问的答案。
$cmp$ 函数实现询问的排序,排序方法可以了解一下以下网站。