优雅暴力算法——莫队
众所周知,莫队是莫涛大神发明的一种玄学优雅暴力算法,鉴定为:区间查询专业对口,拓展应用十分毒瘤。
莫队的模板特别方便记忆,其实只要领悟了莫队的核心思想,可谓是非常简单。
前置芝士
- 分块基础思想
排序和自定义 。
引入
来看一道例题:P1972 [SDOI2009] HH的项链
题目大意:给定一个长度为
首先考虑朴素做法:开一个桶记录区间内每一个数出现的次数,扫一遍
如何优化?
优化1:
在统计时如果当前数出现次数为
优化2:
不难发现,如果每次都暴力扫描每个区间,会浪费浪费掉很多有用的信息。
就比如:首先询问区间
其实我们可以将前面的查询作为基础来求出后面的询问。
我们用两个指针
就比如上面这个例子,只需要在桶中将
这样一看似乎能优化很多,但如果询问的区间为
那么又该怎么优化呢?
考虑将所有操作离线下来,按左端点从小到大的顺序将询问排序,这样左指针的移动次数就降为
还能优化吗?
很遗憾,到目前为止,基础的方法已经无法再进行任何的优化了。但这个时候,莫队的出现,让无数人看到了希望的曙光。
莫队基本思想
莫队说:“我们先对整个序列进行分块,按照左端点所在块的编号从小到大为第一关键字排序,再按照右端点从小到大为第二关键字排序。”然后他以
这样做为什么是对的?
我们来分析一下这样做的时间复杂度:
排序一遍,时间复杂度 。- 设每个块中有
个左端点。在块中,由于左端点不单调,所以最坏情况下要移动 次。在块外,由于左端点单调,所以最多跨越 个块。综上所述,总共会移动 次。 - 每个块中有
个左端点。由于左端点同块的右端点单调,所以最坏情况下会移动 次(相当于移动完整个序列),每个块都是如此,所以右端点总共会移动 次。
综上所述,总时间复杂度为
复杂度降了一个根号!
#include <iostream>
#include <cmath>
#include <algorithm>
using namespace std;
const int N = 1000010;
int n, m;
int a[N], ans[N];
int cnt[N];
int t;
struct node{
int id, l, r;
}q[N];
inline int read() {
int x = 0;
char ch = getchar();
while(ch < '0' || ch > '9') ch = getchar();
while(ch >= '0' && ch <= '9') {x = (x << 3) + (x << 1) + ch - 48; ch = getchar();}
return x;
}
inline int get(int x) {return x / t;}
inline bool cmp(node x, node y) {
if(get(x.l) == get(y.l)) {
if(get(x.l) & 1) return x.r < y.r;
return x.r > y.r;
}
return x.l < y.l;
}
int main() {
n = read();
for(int i = 1; i <= n; i++) a[i] = read();
m = read();
t = (int)sqrt(n);
int l, r;
for(int i = 1; i <= m; i++) {
l = read(), r = read();
q[i] = {i, l, r};
}
sort(q + 1, q + m + 1, cmp);
for(int i = 0, j = 1, k = 1, res = 0; k <= m; k++) {
l = q[k].l, r = q[k].r;
int id = q[k].id;
while(i < r) res += !cnt[a[++i]]++;
while(i > r) res -= !--cnt[a[i--]];
while(j < l) res -= !--cnt[a[j++]];
while(j > l) res += !cnt[a[--j]]++;
ans[id] = res;
}
for(int i = 1; i <= m; i++) printf("%d\n", ans[i]);
return 0;
}
然后你会发现过不了这道题。
实际上洛谷上的这道题卡了莫队,但是运用上地址连续还是可以卡过的,实在不行还是去 ACwing 上提交吧。
注意:若
优化
1.
2. 奇偶排序
这是一个特别玄学的优化。
它的主要原理便是右指针跳完奇数块往回跳时在同一个方向能顺路把偶数块跳完,然后跳完这个偶数块又能顺带把下一个奇数块跳完。理论上主算法运行时间减半,实际情况有所偏差。(不过能优化得很爽就对了)
也就是说,对于左端点在同一奇数块的区间,右端点按升序排列,反之降序。这个东西也是看着没用,但实际效果显著。
inline bool cmp(node x, node y) {
if(get(x.l) == get(y.l)) {
if(get(x.l) & 1) return x.r < y.r;
return x.r > y.r;
}
return x.l < y.l;
}
- 快读快写
莫队进阶应用:带修莫队
如果不光有查询操作,还有修改操作,该怎么办?
单点修改加区间查询,由于莫队是离线算法,所以遇到强制在线就直接去世了。
不过这道题并没有强制在线,我们也可以把所有的操作全部离线下来排序。
但是修改操作怎么办?
因为修改是有顺序的,每次修改只会会对它之后的查询操作有变动,而对它之前的查询不影响,所以我们在普通莫队的基础上再加上一个变量:时间。
令当前时间为
通俗地讲,就是再弄一指针,在修改操作上跳来跳去,如果当前修改多了就改回来,改少了就改过去,直到次数恰当为止。
排序也需要加上时间这一关键字。
注意:带修莫队的块长不是
而且不能使用奇偶排序了。
#include <algorithm>
#include <iostream>
#include <cmath>
using namespace std;
const int N = 140010;
int n, m;
int a[N];
int ans[N], cnt[N << 3];
struct node{
int id, l, r, t;
}q[N]; //查询操作
int ttq;
struct Node{
int p, c;
}cha[N]; //修改操作
int ttc;
int len;
inline int read() {
register int x = 0;
register char ch = getchar();
while(ch < '0' || ch > '9') ch = getchar();
while(ch >= '0' && ch <= '9') {x = (x << 3) + (x << 1) + ch - '0'; ch = getchar();}
return x;
}
inline int get(int x) {return x / len;}
bool cmp(node x, node y) {
if(get(x.l) != get(y.l)) return x.l < y.l;
if(get(x.r) != get(y.r)) return x.r < y.r;
return x.t < y.t;
}
inline void add(int x, int &res) {
cnt[x]++;
if(cnt[x] == 1) res++;
}
inline void del(int x, int &res) {
cnt[x]--;
if(!cnt[x]) res--;
}
int main() {
n = read(), m = read();
for(int i = 1; i <= n; i++) a[i] = read();
char op[2];
int x, y;
for(int i = 1; i <= m; i++) {
scanf("%s", op);
x = read(), y = read();
if(*op == 'Q') {
++ttq;
q[ttq] = {ttq, x, y, ttc};
}
else cha[++ttc] = {x, y};
}
len = cbrt((double)n * n * ttc / m + 1);
if(ttc == 0) len = sqrt((double)n * n / m);
sort(q + 1, q + ttq + 1, cmp);
int id, l, r, tg;
for(int i = 0, j = 1, t = 0, k = 1, res = 0; k <= m; k++) {
id = q[k].id, l = q[k].l, r = q[k].r, tg = q[k].t;
while(i < r) add(a[++i], res);
while(i > r) del(a[i--], res);
while(j < l) del(a[j++], res);
while(j > l) add(a[--j], res);
while(t < tg) {
++t;
if(cha[t].p >= j && cha[t].p <= i) { //如果修改的位置处于这个区间内,修改才有效
del(a[cha[t].p], res);
add(cha[t].c, res);
}
swap(a[cha[t].p], cha[t].c); //将修改加入
}
while(t > tg) {
if(cha[t].p >= j && cha[t].p <= i) {
del(a[cha[t].p], res);
add(cha[t].c, res);
}
swap(a[cha[t].p], cha[t].c);
--t;
}
ans[id] = res;
}
for(int i = 1; i <= ttq; i++) printf("%d\n", ans[i]);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」