莫队
莫队算法 | Mo's Algorithm
前言 | Preface
其实还是分块的思想
莫队就是纯暴力,只不过修改了一下暴力的顺序,从而将复杂度降到了 \(\mathcal O((n+m)\sqrt m)\),\(n\) \(m\) 同阶的情况下就是 \(\mathcal O(n\sqrt n)\)
莫队是离线算法
莫队解决的是区间查询类的问题,所处理的操作多半构成了阿贝尔群,强调逆元(或者说逆操作)的存在性
普通莫队
思路
先考虑一种暴力思路:
对于第一个查询 \(l_1,r_1\),我们将它的答案记录下来
接下来的查询 \((l_i,r_i) \text{ s.t. } i \in [2,m]\),我们在上一个查询的答案的基础上进行更改(补充、删除),得到本次查询的答案
这样子,如果每两个查询之间的差距不大,那么复杂度还是不错的
可是没有如果,考虑下面的情况:
(1, 2) (99999, 100000) (1, 2) (99999, 100000) ...
如果像这样子,那么每次的指针移动 \(\mathcal O(n)\),总的就是 \(\mathcal O(n^2)\),然后挂掉了
优化
既然复杂度需要让每两次相邻的查询差距不大保证,那我们为什么不排个序呢?
在按照左端点所在的块的编号作为第一关键字排序后,我们发现差距显著的减小了
可是右端点如果全是单调递增的话,看起来又要走很远
那么,这个时候就要使用奇偶排序了:
当左端点块的编号 \(\equiv 1 \mod{2}\) 的时候,将右端点升序
相反则将右端点降序
在坐标系上则是天然排好序的
表现出来就是这样子的效果(突然发现 desmos 可以用3D了
黑色代表奇数块(为了方便,将 \(x\) 坐标表示左端点所在的块的编号)是向上“走”的,红色则是偶数,是向下走的
这样子我们发现,算法遍历的查询是按照“Z”字形来的
复杂度分析
参考了一下 OI-Wiki 的证明
假设这里 \(m = \mathcal O(n)\),\(B = \lfloor \sqrt n \rfloor\)
我们假设每一个块中的 \(l\) 最大值为:\(\{m_i \}_{i=1}^{\lceil \sqrt n \rceil}\)
考虑修改 \(r\),由于 \(r\) 的最大是 \(n\),所以这一块的总复杂度是 \(\mathcal O(n\sqrt n)\)
然后考虑修改 \(l\),每一次的时间复杂度为 \(\mathcal O(m_i - m_{i - 1})\)
则在一个块内就是 \(\mathcal O(\sqrt n(m_i - m_{i - 1}))\)
总的就为 \(\mathcal O(\sqrt n \sum\limits_{i=1}^{\lceil \sqrt n \rceil}(m_i - m_{i-1})) = \mathcal O(n\sqrt n)\)
所以最坏情况下复杂度为 \(\mathcal O(n \sqrt n)\)
现在考虑 \(n\) \(m\) 不同阶的情况
先考虑右端点,同一块内,右端点是单调递增的,最多 \(\mathcal O(n)\),总共 \(\frac{n}{B}\),\(T_r(n) = \mathcal O(\dfrac{n^2}{B})\)
然后考虑左端点,每次最多动 \(\mathcal O(B)\) 次,\(m\) 次询问,\(T_l(n) = \mathcal O(mB)\)
总共 \(T(n) = T_r(n) + T_l(n) = \mathcal O(\dfrac{n^2}{B} + mB)\)
由均值不等式易得 \(T_{\min}(n) = \mathcal O(\dfrac{n^2}{\frac{n}{\sqrt m}} + m \cdot \dfrac{n}{\sqrt n}) = \mathcal O((n + m)\sqrt m)\),此时 \(B = \frac{n}{\sqrt m}\)
例题
SPOJ D-Query
这就是一道版子题
View Code
int n, m, block;
struct node{
int left, right;
int id, block_id;
bool operator<(const node& another) const {
if (block_id != another.block_id) return block_id < another.block_id;
return (block_id & 1) ? right < another.right : right > another.right;
}
} query[maxq];
int cnt[V];
int answer = 0;
int l = 1, r = 0;
int a[maxn];
int ans[maxq];
void solve() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
cin >> m;
block = max(1, (int)(n / sqrt(m)));
for (int i = 1; i <= m; i++) {
cin >> query[i].left >> query[i].right;
query[i].id = i;
query[i].block_id = query[i].left / block;
}
sort(query + 1, query + m + 1);
answer = 0;
l = 1, r = 0;
for (int i = 1; i <= m; i++) {
int left = query[i].left, right = query[i].right;
while (l > left) {
l--;
if (cnt[a[l]] == 0) answer++;
cnt[a[l]]++;
}
while (r < right) {
r++;
if (cnt[a[r]] == 0) answer++;
cnt[a[r]]++;
}
while (l < left) {
cnt[a[l]]--;
if (cnt[a[l]] == 0) answer--;
l++;
}
while (r > right) {
cnt[a[r]]--;
if (cnt[a[r]] == 0) answer--;
r--;
}
ans[query[i].id] = answer;
}
for (int i = 1; i <= m; i++) cout << ans[i] << "\n";
}
P2709 【模板】莫队 / 小 B 的询问
每一次的增量是 \(2\text{cnt}_{a[i]}+1\),还是十分的简单(然后没开 long long 被卡爆了
View Code
int n, m, block, k;
struct node{
int left, right;
int id, block_id;
bool operator<(const node& another) const {
if (block_id != another.block_id) return block_id < another.block_id;
return (block_id & 1) ? right < another.right : right > another.right;
}
} query[maxq];
int cnt[V];
ll answer = 0;
int l = 1, r = 0;
ll a[maxn];
ll ans[maxq];
void solve() {
cin >> n;
cin >> m;
cin >> k;
for (int i = 1; i <= n; i++) cin >> a[i];
block = max(1, (int)(n / sqrt(m)));
for (int i = 1; i <= m; i++) {
cin >> query[i].left >> query[i].right;
query[i].id = i;
query[i].block_id = query[i].left / block;
}
sort(query + 1, query + m + 1);
answer = 0;
l = 1, r = 0;
for (int i = 1; i <= m; i++) {
int left = query[i].left, right = query[i].right;
while (l > left) {
l--;
answer += 2ll * cnt[a[l]] + 1;
cnt[a[l]]++;
}
while (r < right) {
r++;
answer += 2ll * cnt[a[r]] + 1;
cnt[a[r]]++;
}
while (l < left) {
cnt[a[l]]--;
answer -= 2ll * cnt[a[l]] + 1;
l++;
}
while (r > right) {
cnt[a[r]]--;
answer -= 2ll * cnt[a[r]] + 1;
r--;
}
ans[query[i].id] = answer;
}
for (int i = 1; i <= m; i++) cout << ans[i] << "\n";
}
P1494 [国家集训队] 小 Z 的袜子
这是道经典的题目,据说莫队就是从这道题中获得了灵感
其实很简单,抽到两只相同颜色的袜子的概率是 \(\dfrac{n_i (n_i - 1)}{l(l-1)}\),\(n_i\) 表示颜色 \(i\) 的出现次数,\(l\) 表示区间的长度
还是很好维护的(每一次减去原本的,更新完了后再加回来
View Code
int n, m, block, k;
struct node{
int left, right;
int id, block_id;
bool operator<(const node& another) const {
if (block_id != another.block_id) return block_id < another.block_id;
return (block_id & 1) ? right < another.right : right > another.right;
}
} query[maxq];
int cnt[V];
ll answer = 0;
int l = 1, r = 0;
int a[maxn];
struct factor{
ll u, d;
}ans[maxq];
void add(int value) {
answer -= 1ll * cnt[value] * (cnt[value] - 1);
cnt[value]++;
answer += 1ll * cnt[value] * (cnt[value] - 1);
}
void erase(int value) {
answer -= 1ll * cnt[value] * (cnt[value] - 1);
cnt[value]--;
answer += 1ll * cnt[value] * (cnt[value] - 1);
}
void solve() {
cin >> n;
cin >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
block = max(1, (int)(n / sqrt(m)));
for (int i = 1; i <= m; i++) {
cin >> query[i].left >> query[i].right;
query[i].id = i;
query[i].block_id = query[i].left / block;
}
sort(query + 1, query + m + 1);
answer = 0;
l = 1, r = 0;
for (int i = 1; i <= m; i++) {
int left = query[i].left, right = query[i].right;
while (l > left) add(a[--l]);
while (r < right) add(a[++r]);
while (l < left) erase(a[l++]);
while (r > right) erase(a[r--]);
ans[query[i].id] = {answer, 1ll * (right - left + 1) * (right - left)};
}
for (int i = 1; i <= m; i++) {
if (ans[i].u == 0) {
cout << "0/1\n";
continue;
}
cout << ans[i].u / __gcd(ans[i].u, ans[i].d) << "/" << ans[i].d / __gcd(ans[i].u, ans[i].d) << "\n";
}
}
带修改的莫队
很明显,刚才的操作是不支持修改操作的
如果有修改操作怎么办?
受着
思路
我们想一想为什么不能够进行修改操作?
因为多个查询之间可能混入了修改操作
嗯,如果我们把操作的时间记录下来不就行了吗?
我们队查询操作记录一个 \(t\) 表示在这个操作之前有 \(t\) 个修改操作
这样子,每一次就将动态的将数组进行修改不就行啦
但是如果这样子,我们就需要对排序逻辑进行修改
struct node{
int left, right;
int id, block_id_left, block_id_right; // 该查询的 id, left 的块的 id,right 的块的id
int time;
bool operator<(const node& another) const {
if (block_id_left != another.block_id_left) return block_id_left < another.block_id_left;
if (block_id_right != another.block_id_right) return (block_id_left & 1) ? block_id_right < another.block_id_right : block_id_right > another.block_id_right;
return (block_id_right & 1) ? time < another.time : time > another.time;
}
};
注意!这样只能够处理单点修改
时间复杂度分析
考虑块长为 \(B\)
共有 \(\lceil \dfrac{n}{B} \rceil\) 个块
考虑左右的端点不换块(换块了可以忽略不计
用 \(n_{i,j}\) 表示左右端点分别在第 \(i\) 个、第 \(j\) 个块中的个数
得到
要让这个最小,考虑求导
令 \(f(B) = mB + \dfrac{n^2t}{B^2}\),则 \(f'(B) = m - \dfrac{2n^2t}{B^2} = 0\)
解得 \(B_0 = \dfrac{2^{\frac13}n^{\frac23}t^{\frac13}}{m^{\frac13}}\)
则最优复杂度为:\(\mathcal O(n^{\frac23}m^{\frac23}t^{\frac13})\)
当 \(n,m,t\) 同阶,则为 \(\mathcal O(n^{\frac35})\)
例题
P1903 【模板】带修莫队 / [国家集训队] 数颜色 / 维护队列
就是刚才的思路
View Code
struct Modify{
int position, value;
} modify[maxq];
int cnt[V];
ll answer = 0;
int l = 1, r = 0, t;
int a[maxn];
int ans[maxq];
void add(int value) {
if (cnt[value] == 0) answer++;
cnt[value]++;
}
void erase(int value) {
cnt[value]--;
if (cnt[value] == 0) answer--;
}
void travel(int t, int l, int r) {
int posi = modify[t].position;
if (l <= posi and posi <= r) {
erase(a[posi]);
add(modify[t].value);
}
swap(a[posi], modify[t].value);
}
void solve() {
cin >> n;
cin >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
block = max(1, int(pow(n, 2.0 / 3.0)));
int modify_cnt = 0, query_cnt = 0;
for (int i = 1; i <= m; i++) {
char ch;
cin >> ch;
if (ch == 'Q'){
query_cnt++;
cin >> query[query_cnt].left >> query[query_cnt].right;
query[query_cnt].time = modify_cnt;
query[query_cnt].id = query_cnt;
query[query_cnt].block_id_left = query[query_cnt].left / block;
query[query_cnt].block_id_right = query[query_cnt].right / block;
} else {
modify_cnt++;
cin >> modify[modify_cnt].position >> modify[modify_cnt].value;
}
}
sort(query + 1, query + query_cnt + 1);
answer = 0;
l = 1, r = 0, t = 0;
for (int i = 1; i <= query_cnt; i++) {
int ql = query[i].left, qr = query[i].right, qt = query[i].time;
while (t < qt) travel(++t, l, r);
while (t > qt) travel(t--, l, r);
while (l > ql) add(a[--l]);
while (r < qr) add(a[++r]);
while (l < ql) erase(a[l++]);
while (r > qr) erase(a[r--]);
ans[query[i].id] = answer;
}
for (int i = 1; i <= query_cnt; i++) cout << ans[i] << "\n";
}
这个 travel 起到的就是将自己的时间回溯或者跳跃到合适的位置(中间进行 modify 操作,也就是修改,这样子也可以撤回)
其余
回滚莫队
引入
回滚(Rollback)指的是程序或数据处理错误,将程序或数据恢复到上一次正确状态的行为。回滚包括程序回滚和数据回滚等类型。
删除由一个或多个部分完成的事务执行的更新。为保证应用程序、数据库或系统错误后还原数据库的完整性,需要使用回滚。
回滚泛指程序更新失败, 返回上一次正确状态的行为。
回滚对程序员意味着非常严重的失误。
所以回滚次数往往与程序员的薪金直接联系。主流互联网公司通常都将回滚定位为最严重的事故。
回滚与恢复有本质的区别。
回滚莫队将操作由群放轻到了半群,也就是不一定存在逆元(逆操作)
不存在逆元也就说明了我们的删除或者增加有一个操作会不行
思路
考虑只有增加操作的情况(反之亦然
考虑左端点在同一块内的查询,这个时候的查询像这样子:(中间的竖线代表这个块的右端点
可以发现,由于排序的原因,右端点都是单调的
这说明了什么,说明了我们的右端点是可以不断拓展而不用涉及逆操作的!
什么,你说左端点无序?
那你就没有看全,由于左端点一定只在一个块内,所以我们可以直接暴力回滚,最多也就 \(\mathcal O(\sqrt n)\) 的复杂度
如果觉得太慢了,那么可以考虑将值赋给局部变量进行操作,这样子就不用回滚了
具体的,将 l、r 的初始值赋值为 block[i].right + 1 以及 block[i].right
由于在同一个块内,qr 一定是单调递增的,所以我们就可以一直扩大(r++
而左端点,每一次操作完,就要回滚到 block[i].right + 1,在回滚的过程中将操作栈内的操作回溯
注意,右端点不能够使用奇偶排序,否则右端点的性质就没了
例题
P14420 [JOISC 2014] 历史的研究 / Historical Research
很版的一道题(感觉比模版还要版
这里贴出来主要代码,然后分析
ll calc(int l, int r) {
ll result = 0;
for (int i = l; i <= r; i++) tmp_cnt[a[i]] = 0;
for (int i = l; i <= r; i++) {
tmp_cnt[a[i]]++;
result = max(result, 1LL * origin[a[i]] * tmp_cnt[a[i]]);
}
for (int i = l; i <= r; i++) tmp_cnt[a[i]] = 0;
return result;
}
for (int i = 1; i <= block_count; i++) {
memset(cnt, 0, sizeof(int) * (n + 5));
int l = block[i].right + 1;
int r = block[i].right;
ll now_answer = 0;
while (j <= m and query[j].block_id == i) {
if (id[query[j].right] == i) {
ans[query[j].id] = calc(query[j].left, query[j].right);
j++;
continue;
}
int ql = query[j].left, qr = query[j].right;
while (r < qr) {
r++;
cnt[a[r]]++;
now_answer = max(now_answer, 1LL * origin[a[r]] * cnt[a[r]]);
}
ll temp_ans = now_answer;
while (l > ql) {
l--;
cnt[a[l]]++;
now_answer = max(now_answer, 1LL * origin[a[l]] * cnt[a[l]]);
}
ans[query[j].id] = now_answer;
while (l <= block[i].right) {
cnt[a[l]]--;
l++;
}
now_answer = temp_ans;
j++;
}
}
calc 函数十分简单,就是两个端点在同一块内,就直接暴力计算了
while (r < qr) {
r++;
cnt[a[r]]++;
now_answer = max(now_answer, 1LL * origin[a[r]] * cnt[a[r]]);
}
右端点的拓展
ll temp_ans = now_answer;
while (l > ql) {
l--;
cnt[a[l]]++;
now_answer = max(now_answer, 1LL * origin[a[l]] * cnt[a[l]]);
}
ans[query[j].id] = now_answer;
while (l <= block[i].right) {
cnt[a[l]]--;
l++;
}
now_answer = temp_ans;
向左拓展 l 并且回滚
P5906 【模板】回滚莫队&不删除莫队
这才是模版
有了上一道题,做这个还是很简单的
View Code
int calc(int left, int right) {
int result = 0;
for (int i = left; i <= right; i++) last[a[i]] = 0;
for (int i = left; i <= right; i++) {
if (last[a[i]]) result = max(result, i - last[a[i]]);
else last[a[i]] = i;
}
return result;
}
for (int i = 1, j = 1; i <= block_count; i++){
int l = block[i].right + 1, r = block[i].right, now_answer = 0;
top = 0;
for (; query[j].block_id == i; j++) {
if (id[query[j].right] == i) {
ans[query[j].id] = calc(query[j].left, query[j].right); // in the same block
continue;
}
int ql = query[j].left, qr = query[j].right;
while (r < qr) {
r++;
__end[a[r]] = r;
if (!start[a[r]]) start[a[r]] = r, clear_stack[++top] = a[r];
now_answer = max(now_answer, r - start[a[r]]);
}
int temp = now_answer;
while (l > ql) {
l--;
if (!__end[a[l]]) __end[a[l]] = l;
now_answer = max(now_answer, __end[a[l]] - l);
}
ans[query[j].id] = now_answer;
while (l <= block[i].right) {
if (__end[a[l]] == l) __end[a[l]] = 0;
l++;
}
now_answer = temp;
}
for (int k = 1; k <= top; k++) start[clear_stack[k]] = __end[clear_stack[k]] = 0;
}
其余
- ZQUERY - Zero Query 和
P5906挺像的 - P4137 Rmq Problem / mex 这个就是只能够删除,初始
l、r是1和n,每次更新为:
l = block[i].left;
r = n;
- P8078 [WC2022] 秃子酋长 这个用一个链表维护一下有序的数就行了(调了半天,感觉要成秃子🐰了
树上莫队
引言
树是一种非线性拓扑结构,应用于线性操作的莫队应该是用不上去的,吧?
当然可以,回想一下,我们学过 DFS 序和欧拉序,都可将树压成一个序列
可是 DFS 序上面的路径是不连续的,这怎么办?
你是不是忘了欧拉序,路径在他上面可是连续的
接下来,用 \(\text{ance}(u)\)/ance(u) 表示节点 \(u\) 的祖先,\(\text{lca}(u,v)\)/lca(u,v) 表示 \(u\) 和 \(v\) 的最近公共祖先
用 in[u] 表示 \(u\) 的 DFS 序,out[u] 表示 \(u\) 在欧拉序中的第二次出现
可以用下面代码求出:
void dfs(int u, int father) {
euler[++timer] = u;
in[u] = timer;
depth[u] = depth[father] + 1;
jump[u][0] = father;
for (int i = 1; i < 17; i++) jump[u][i] = jump[jump[u][i - 1]][i - 1];
for (int v : adj[u]) if (v != father) dfs(v, u);
euler[++timer] = u;
out[u]= timer;
}
接下来,我们看路径 \(u \rightsquigarrow v\) 如何表示
- \(u \in \text{ance}(v)\),表示成
[in[u], in[v]] - \(u \not\in \text{ance}(v)\),表示成
[out[u],in[v]]
如果觉得不对,可以自己下去验证,我就不证啦(其实是懒得证
注意第二种情况,lca(u, v) 是没有包含进去的,要加上
然后就可以快乐地在树上进行操作啦
例题
二次离线莫队
引入
有些时候,增加和删除操作的效率介于一个十分奇怪的区间,用普通莫队、回滚莫队时间复杂度都很高
所以我们也就需要另一种算法
思路
定义函数 \(f:\Z \times (\Z \times \Z) \longrightarrow \Z\):
\(f(x, [l,r])\) 表示 \(x\) 对于区间 \([l,r]\) 的贡献
所以容易知道:
对于 \(f(a_{r+1},[1,r])\),由于是直接从 \(r\) 拓展的,计算起来十分简单
所以考虑 \(f(a_{r+1}, [1,l-1])\),这里我们将 \([1,l-1]\) 再看做一次询问,然后存下来离线处理
这就是第二次离线
通过这种方式,莫队的时间复杂度降到了 \(\mathcal{O} (nq+n\sqrt n)\),其中 \(\mathcal O(q)\) 是拓展/删除的复杂度
例题
P4887 【模板】莫队二次离线 / 第十四分块(前体)
模版题
按照上面我们说的步骤去做就行了
int n, m, k;
int a[maxn];
int xor_freq[(1 << 14) + 1];
vector<int> valid_target;
int prefix[maxn];
int prefix_c_self[maxn];
ll sum_prefix[maxn];
ll sum_prefix_c_self[maxn];
int block;
struct node{
int l, r, id;
bool operator<(const node& other) const {
if (l / block != other.l / block) return l < other.l;
return ((l / block) & 1) ? r < other.r : r > other.r;
}
} query[maxn];
struct pack{
int l, r, id, sign;
};
vector<pack> deferred[maxn];
ll static_differences[maxn];
ll dynamic_difference[maxn];
ll answer[maxn];
void solve() {
cin >> n >> m >> k;
for (int i = 1; i <= n; i++) cin >> a[i];
if (k > 14) {
for (int i = 1; i <= m; i++) {
int l, r;
cin >> l >> r;
cout << 0 << "\n";
}
return ;
}
for (int i = 0; i < (1 << 14); i++)
if (__builtin_popcount(i) == k)
valid_target.push_back(i);
for (int i = 1; i <= n; i++) {
prefix[i] = xor_freq[a[i]];
for (int target : valid_target)
xor_freq[target ^ a[i]]++;
prefix_c_self[i] = xor_freq[a[i]];
sum_prefix[i] = sum_prefix[i - 1] + prefix[i];
sum_prefix_c_self[i] = sum_prefix_c_self[i - 1] + prefix_c_self[i];
}
for (int i = 1; i <= m; i++) {
cin >> query[i].l >> query[i].r;
query[i].id = i;
}
block = max(1, (int)(n / sqrt(m)));
sort(query + 1, query + m + 1);
int l = 1, r = 0;
for (int i = 1; i <= m; i++) {
int ql = query[i].l, qr = query[i].r;
int id = query[i].id;
if (l > ql) {
static_differences[i] -= (sum_prefix_c_self[l - 1] - sum_prefix_c_self[ql - 1]);
deferred[r].push_back({ql, l - 1, id, 1});
l = ql;
}
if (r < qr) {
static_differences[i] += (sum_prefix[qr] - sum_prefix[r]);
deferred[l - 1].push_back({r + 1, qr, id, -1});
r = qr;
}
if (l < ql) {
static_differences[i] += (sum_prefix_c_self[ql - 1] - sum_prefix_c_self[l - 1]);
deferred[r].push_back({l, ql - 1, id, -1});
l = ql;
}
if (r > qr) {
static_differences[i] -= (sum_prefix[r] - sum_prefix[qr]);
deferred[l - 1].push_back({qr + 1, r, id, 1});
r = qr;
}
}
fill(xor_freq, xor_freq + (1 << 14) + 1, 0);
for (int i = 1; i <= n; i++) {
for (int target : valid_target) xor_freq[a[i] ^ target]++;
for (const pack& p : deferred[i]) {
ll summation = 0;
for (int j = p.l; j <= p.r; j++) summation += xor_freq[a[j]];
dynamic_difference[p.id] += p.sign * summation;
}
}
ll current_answer = 0;
for (int i = 1; i <= m; i++) {
current_answer += static_differences[i] + dynamic_difference[query[i].id];
answer[query[i].id] = current_answer;
}
for (int i = 1; i <= m; i++) cout << answer[i] << "\n";
}
代码中,static_difference 代表 \(f(a_{r+1},[1,r])\) 这一类的答案
dynamic_difference 代表 \(f(a{r+1},[l,r])\) 这一类的答案,作为第二次的查询
其余
- P5047 [Ynoi2019 模拟赛] Yuno loves sqrt technology II
- P5501 [LnOI2019] 来者不拒,去者不追
- P5398 [Ynoi2018] GOSICK
居然全都是紫起步,且都是 Ynoi 的

浙公网安备 33010602011771号