#edu3021. 【教程】莫队入门
【教程】莫队入门
Update 2026.1
Update 2025.3.5 : Luogu博客需要科技才能访问,无奈只能搬运一下,这是之前的入门学习材料
莫队算法是一种可以解决大部分区间离线算法的离线算法,核心思想是分块。 一般情况下时间复杂度为 ,超好写,是解决问题的很优雅的暴力方法。(当然毒瘤出题人也会卡莫队,比如强制在线,或复杂度 )
普通莫队
问题引入
假如有这样一道题:对于一个数列,每次给出一个询问 ,求 的区间和。(强制使用莫队来做)
【分析】
假设你现在已经知道区间 的区间和,求区间 区间和,怎么求?显然只需要在 的基础上加上第 项的贡献。求区间 的区间和,可以在区间 和的基础上,加上第 项的贡献,减去第 项的贡献,也就是当前区间的值是上一步区间左右端点一步一步挪动过去的。
对于当前区间 ,可以分四种情况讨论挪动过程
-
1.加上当前区间左边一格的贡献:
-
2.加上当前区间右边一格的贡献:
-
3.减去当前区间左边一格的贡献:
-
4.减去当前区间右边一格的贡献:
注意:区间先扩大,再缩小,想一想为什么?(防止 )
当时这样做显然会超时,时间复杂度是
如果我们询问的顺序如下:
时间复杂度 ,显然 TLE
但是如果修改一下询问顺序:
效率就会大大不同,也就是询问顺序会影响时间复杂度
莫涛大佬套了分块,通过巧妙安排询问顺序,优化了时间复杂度。
排序规则
对于一个询问 ,对于 按照其所在块排序,对于 在同一块内的,按照 的大小排序。
即第一关键字是 所在块,第二关键字是 。
莫队主体框架是一样的,区别在于 和 函数
主体框架
int a[N],n,m,k,cnt[N],len;
ll res,ans[N];
struct node{
int l,r,id;
}q[N];
cin>>n>>m>>k;
len=sqrt(n); //块长
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=m;i++)cin>>q[i].l>>q[i].r,q[i].id=i;
sort(q+1,q+m+1,cmp); //按照规则排序
int l=1,r=0;
for(int i=1;i<=m;i++)
{
while(q[i].l<l)add(--l);
while(q[i].r>r)add(++r);
while(q[i].l>l)sub(l++);
while(q[i].r<r)sub(r--);
ans[q[i].id]=res; //保留答案
}
for(int i=1;i<=m;i++)
cout<<ans[i]<<endl;
自定义排序
自定义函数修改如下:
bool cmp(node a,node b)
{
return (a.l/len==b.l/len)?(a.r<b.r):(a.l<b.l);
}
复杂度分析
序列长度为 ,询问次数为 ,设块长为 ,分成 块。
-
1.右端点 的移动总次数:
排序后,同一左块内的所有查询,右端点是升序排列的(第二关键字升序),因此对每个块,右端点 移动总长度为 , 总共有 个块,那么总移动次数就是 。
-
2.左端点 的移动总次数:
排序后,查询按左块升序排列,分两种情况分析 移动:
同一块内的查询:左端点 都在同一个大小为 的块内,因此处理该块内所有查询时, 最多在块内移动 次(从块的一端到另一端),单次块内 移动次数上界为 ;
跨块的查询:从第 块的查询切换到第 块的查询时, 最多从第 块的任意位置移动到第 块的任意位置,移动次数仍为 (块大小为 ); 总共有 个查询,所有情况的 单次移动均不超过 ,因此整体移动总次数上界为:。
那么总时间复杂度为:
,根据不等式,当 时不等式区最小值,此时 ,时间复杂度为 。
当 与 数量级相同, ,时间复杂度为
其他情况,块长应取
奇偶优化
对于所有询问,为了减少 无用移动,当 所在的块是奇数时按照 从小到大,否则从大到小排序。
这是一种常数优化,并不影响时间复杂度。
如果加上奇偶优化,排序函数如下:
bool cmp(node a,node b)
{
if(a.l/len==b.l/len)return (a.l/len&1)? a.r>b.r : a.r<b.r;
return a.l<b.l;
}
几何解释
每一个询问 可以看作在二维平面上的一个点,从一个询问到另外一个询问,相当于两个点之间的曼哈顿距离。
如果按照横坐标对所有询问进行排序,访问顺序如下图,时间复杂度是 。
暴力访问
按照左端点所在块访问,块内按照右端点排序,访问顺序如下:
莫队访问
如果带上奇偶优化,访问顺序如下:
带奇偶优化的莫队访问
例1. P3901 数列找不同
【题意简化】给定一个长度为 的整数序列 , 次询问,询问区间 内所有数是否互不相同。
【分析】莫队基础题。利用计数数组,记录每个数在询问区间出现的次数,所有询问按照莫队方式离线排序。当一个数需要加入计数数组和重复的种类数 cnt,若计数个数从 1 增加为 2 ,说明此时有 2 个相同的数,重复种类数 cnt加 1 。当删除一个数 ,若计数个数从 2 减 1 ,说明此时这个数,从重复到不重复,cnt 减少 1 。从一个询问区间移动到另外一个询问区间,cnt 如果为 0 ,说明此询问区间没有相等的数,否则有相等的数。
【参考代码】点击
例2. SP3267 DQUERY - D-query
【题意简化】给定 个整数 , 次询问,询问区间 内有多少不同的数字。$(1 \le n \le 3\times 10^4,1 ≤ q ≤ 2\times 10^5, 1\le a_i \le 10^6)$
【分析】对所有询问离线排序。定义一个全局计数数组,记录值为 出现的次数。增加数时,若计数次数从 增加到 ,说明增加了一个不同的数字,答案加 ;当删除一个数时,若计数次数从 减少到 ,说明减少了一个不同的数字,答案减 。
本题是弱化版HH的项链,“HH的项链”数据加强,莫队会超时,可以利用这个题目学习莫队入门。
【参考代码】点击
例3. P2709 【模板】莫队 / 小B的询问
【题意简化】给定一个长为 的整数序列 ,值域为 。 次询问,每个询问给定一个区间 ,求:
其中 表示数字 在 中的出现次数。
【分析】莫队的经典板子题,增加或减掉一个点,那么就减掉之前的贡献,加上新贡献。
增加一个数 ,那么对答案的贡献增加 。同理减少一个数去掉减少对答案的贡献。
void add(int x)
{
cnt[a[x]]++;
res+=(cnt[a[x]]*cnt[a[x]])-(cnt[a[x]]-1)*(cnt[a[x]]-1);
}
void sub(int x)
{
cnt[a[x]]--;
res-=(cnt[a[x]]+1)*(cnt[a[x]]+1)-(cnt[a[x]]*cnt[a[x]]);
}
【参考】:点击
例4. [ABC293G] Triple Index
【题意简化】给定一个长度为 的正整数序列 ,以及关于该序列的 个查询。
对于每个 ,第 个查询给出一对整数 ,请输出满足以下两个条件的整数三元组 的个数:
数据规模:
【分析】如果增加一个数 , 计数个数从 个变为 ,对答案贡献增加:
同理减少一个数,从 个变为 ,对答案减少:
【参考代码】点击
例5. P1494 小Z的袜子
【题意简化】给定一个长度为 的序列,多次询问 任意抽中两个相同的数概率。
【分析】对所有操作进行分块,也就是莫队。
单点移动需要计算,区间 总共的方法数为 。
抽中相同的方法数为 , 表示 的个数。
当新增 , 抽中相同的方法数 ,那对答案的增量就是 , 那 ,然后 .
当删除 ,那抽中相同的方法数 , 那减少量就是 . 先 , 相同的方法数 .
【参考代码】点击
例6. CF617E XOR and Favorite Number
【题意简化】给定一个长度为一个长度为 的数组 和整数 。现在他要求你回答 个询问。每个询问由一对 和 给出,问你在区间 内,有多少组整数对 ,使得数列 的异或和等于 。
,
【分析】对于 可以使用前缀异或和表示 。
对于新增位置 , 以 a[p] 结尾考虑 区间 [j,p] 异或和表示为 ,可以表示为 ,即只需要知道有多少个前缀和等于 ,就可以计算出以 a[p] 为结尾的符合条件的子数组。注意 ,那么计数要算完后再增加。
同理,对于减少位置 , 对于区间 [p,j] ,去掉 的贡献,区间 [p,j] 的区间和 ,表示为 ,需要知道有多少个 ,那么对于询问区间 l-1.
【参考代码】点击
例7. P5268 [SNOI2017] 一个简单的询问
【题意简化】
给你一个长度为 的序列 ,,和 组询问,每组询问读入 ,需输出
$$\sum\limits_{x=0}^\infty \text{get}(l_1,r_1,x)\times \text{get}(l_2,r_2,x)$$表示计算区间 中,数字 出现了多少次。
,,,
【分析】对于 可以表示为 . 记 为 , 那么可以得到
$$\sum{get(l_1,r_1,x)get(l_2,r_2,x)}=\sum {(P(r_1,x)-P(l_1-1,x))*(P(r_2,x)-P(l_2-1,x))}=\sum(P(r_1,x)*P(r_2,x)-P(r_1,x)*P(l_2-1,x)-P(l_1,x)*P(r_2,x))+P(l_1-1,x)*Pl_2-1,x)$$将每个询问分成 部分,考虑每个数出现次数在 与 出现次数的乘积。使用两个计数数组 cntl[x] 和 cntr[x] 分别表示 x 在 [1,l] 和 [1,r] 中出现的次数. 考虑 移动增加贡献,对于 [1,r] 计数数组 cntr[a[r+1]]++ ,对结果贡献是 [1,l] 出现的次数,结果增加 cntl[a[r+1]],同理减少。
时间复杂度 ,本题还可以直接分块做,可以思考如何分块解决。
【参考代码】点击
其他题目
CF86D Powerful array
P3604 美好的每一天
其他莫队算法:带修莫队、树上莫队、二次离线、回滚莫队
学习完毕
{{ select(1) }}
- YES
- NO