今天来学习莫队
是的,主播在每一期都会留点文末小记,不会有人不知道吧
这又是一个大学生主波的日常产出,结合洛谷P10288讲解了一下莫队算法,洛谷的这道题目不是经典的莫队算法题,但是看到题解上也有一种莫队的做法,所以来学习和总结一下
首先我们来了解一下大佬是怎么把自己搞成一个算法的
莫队,(Mo’s Algorithm),从英文名中我们可以看见这是一个由个人提出并且以个人的名字进行命名的算法(再膜拜一下大佬)
莫队算法由中国信息学竞赛选手莫涛提出,他在算法竞赛领域活跃于 2000 年代末至 2010 年代初,曾多次在国际信息学奥林匹克竞赛(IOI)和 ACM 国际大学生程序设计竞赛(ACM-ICPC)中取得优异成绩。
算法的核心思想涉及将查询排序后按特定顺序处理,类似于队列的操作顺序,莫队算法因其优雅的思想和广泛的适用性,成为了算法竞赛中的经典算法之一,尤其在处理区间查询问题(如区间众数、区间和等)时表现出色。它的核心思想(分块 + 双指针滑动)也启发了许多后续算法的优化和变种。
接下来我们结合题目来了解一下莫队算法的基本思想
题目概述
对于一个数列,每次询问给出一个区间[l,r],询问在这个区间上数x出现的次数
注意观察数据范围
对于全部的
n
n
n有
n
<
=
10
5
n <= 10^5
n<=105,
q
<
=
10
5
q<=10^5
q<=105,数组中
a
i
a_i
ai的最大值满足
a
i
<
10
9
a_i<10^9
ai<109,
T
<
=
5
T<=5
T<=5
算法分析
对于给出的询问中,如果我们直接按照询问区间进行遍历,最暴力的解法,但是时间复杂度肯定不够优,如果可以在前面已经统计过的询问的区间的基础上,通过对已知区间范围的拓展,在这个拓展的过程中完成统计,那么效率应该会高很多
但是事实上我们考虑极端情况:
从区间[1,2]和[n-1,n]之间反复横跳,那么每次更新当前的查询区间的时候几乎会遍历整个数组,这么做时间复杂度还是很高,所以我们可以考虑优化查询的顺序,这就出现了离线查询
具体来说,这些查询相互之间不会对结果造成影响,那么我们就可以主动的改变查询的顺序,以期望我们在查询的过程中所用的时间最优,这就是离线查询,我们可以把所有的查询储存起来(包括查询的具体内容和当前这个查询的编号),之后通过特定的排序方式确定他们新的顺序,从而实现区间拓展式查询的最优查询效果,这就是莫队算法的基本思路
那么怎么来进行排序呢?这里采用了分块排序的思想,首先对整个数组进行分块,取块的大小为 k = n k = \sqrt{n} k=n,首先通过一遍循环确定每一个元素所属的分块,并使用pos[]数组记录下来,先对查询区间的左端点按照所属的分块进行排序,左端点在同一个分块内的,再根据右端点进行排序,奇数分块按照右端点降序排列,偶数分块按照右端点升序排列,这样可以使得拓展区间的左右指针移动次数最小
对于已有的答案区间 [ l , r ] [l,r] [l,r]的拓展过程,就是通过左右指针的自加自减的操作更新已储存的答案的过程,这样的拓展更新相邻位置的答案的操作是 O ( 1 ) O(1) O(1)的
考虑到我们的数据可能是分散排布的,那么在分块的过程中很有可能会出现不均匀分块或者分块长度过大导致块内数据稀疏的问题,这里就用到了离散化的解决方式,把数组中的数据进行离散化处理,使得原数据变成排序并去重之后的对应下标,这样就把原数据放缩到了[1~n]的范围内,这里的n是出现过的不同数值的数目,之后再对离散化后的数据进行分块操作,就可以很好的处理这个问题
离散化处理:
离散化通常需要预先知道所有数据,因此适用于离线处理查询
时间复杂度:
莫队算法的时间复杂度是
O
(
(
n
+
q
)
×
n
)
O((n+q)\times \sqrt{n})
O((n+q)×n),适用于n和q都是
10
5
10^5
105级别的数据
算法实现
下面我们分步实现这个算法,先考虑t组数据,n个数和q个查询的实现
#include<iostream>
#include<vector>
#include<cmath>
#include<algorithm>
using namespace std;
struct Q{
int l,r,x;
int id;
};
int main(){
int t;cin>>t;
while(t--){
int n;cin>>n;
vector<int> a(n+1,0);
vector<int> b(n+1,0);
for(int i=1;i<=n;i++){//记得是从1开始储存的数据
scanf("%d",&a[i]);
b[i]=a[i];
}
sort(b.begin()+1,b.end());//默认从小到大排序
vector<int> c(n+1,0);
int len=0;
c[++len]=b[1];
for(int i=2;i<=n;i++){
if(b[i]==b[i-1])continue;//很奇妙的去重操作
c[++len]=b[i];
}
for(int i=1;i<=n;i++){
a[i]=lower_bound(c.begin()+1,c.begin()+len+1,a[i])-c.begin();
//lower_bound适用于单调递增的数组中,用于寻找第一个大于等于给定数值的数
}
int q;cin>>q;
vector<Q> ques(q+1);
for(int i=1;i<=q;i++){
scanf("%d %d %d",&ques[i].l,&ques[i].r,&ques[i].x);
ques[i].id=i;
}
vector<int> pos(n+1,0);
int k=sqrt(n);
for(int i=1;i<=n;i++){
pos[i]=i/k+1;//调整分块的下标从1开始
}
sort(ques.begin()+1,ques.end(),[&pos](Q &x,Q &y)->bool{
if(pos[x.l]!=pos[y.l])
return pos[x.l]<pos[y.l];
else{
if(pos[x.l]%2==0){
return x.r<y.r;
}else{
return x.r>y.r;
}
}
});
int l=1,r=0;//在一定范围内维护一个cnt数组
vector<int> cnt(n+1,0);
vector<int> ans(q+1,0);
for(int i=1;i<=q;i++){
Q x=ques[i];
while(l<x.l)cnt[a[l++]]--;
while(l>x.l)cnt[a[--l]]++;
while(r<x.r)cnt[a[++r]]++;
while(r>x.r)cnt[a[r--]]--;
int idx = lower_bound(c.begin()+1, c.begin()+len+1, x.x) - c.begin();
if(idx<=len&&c[idx] == x.x) {//如果超出范围了那么返回的是len+1
ans[x.id] = cnt[idx]; // 只有当值真正存在时才计数
} else {
ans[x.id] = 0; // 否则结果为0
}
}
for(int i=1;i<=q;i++){
cout<<ans[i]<<endl;
}
}
}
在改了很多遍之后终于过了,这里的代码有很多细节,我都写在了注释里了
暂时先写到这里,因为主波实在太想发博客了,所以决定写完之后就立马发布出来了hhh
写在后面
感觉各种AI大模型的出现真的极大改变了学习的一些方式
印象里最深刻的还是之前高中学算法的时候学习资源的匮乏,要么是根本没有能查得到的资料,要么是找到了的东西啃不动,但是AI的突然爆发真的彻底改变了这种局面,以前甚至想都没想过能有一个全知全能的LLM给你讲任何你想学的题,任何你想知道的内容,真的完全不可思议,我记得以前最调侃的一句话就是“老师会的不如你们多”,hh,老师会的确实不如我们多,因为老师只负责教会c++是什么,剩下的都是自己自学的,好在有薪火相承的环节,老学长会讲给下一届的我们听,当初的他们也都是再上一届的人教会的,在这一点上来说,我觉得我们高中做的还是挺好的,确实能算有一种共同体的精神,虽然再老一点的大学学长讲课就要收费了(
×
\times
×),而且收费还很高,那次一个寒假上了几节课就每个人收了两千块(
×
\times
×),钱包幻痛
最后,现在关注以后就是老粉了,欢迎给主播点点收藏点点关注~
也非常欢迎大家和博主留言,我们一起进步!

437

被折叠的 条评论
为什么被折叠?



