在线莫队学习笔记

· · 算法·理论

前言

(似乎叫诗乃莫队)

其实刚学莫队的时候就听说这个了,一直想学来着,在学了主席树后更想学了,然后……

一直到现在我都没学,但是很好造,直接造出来了,你来你也行。

下文分别是几个经典莫队的在线化改造。分别是莫队,带修莫队,回滚莫队,树上莫队。最后是一点点的卡常。

然后前两篇代码因为还处于摸索阶段所以码比较丑,最后一篇代码我还是比较满意的。

正文

先来按着我的理解概述一下在线莫队吧,是一个编码容易但是编码较长,常数较大但是常数好卡的东西。唯一较大的缺陷就是空间好劣……

例题:疯狂的颜色序列

一句话题意:区间数颜色,在线。

这个是出现在我们学校题库主席树专题里的题目,当时就非常想写莫队,可惜啊……

不过现在也来得及。

说是莫队,其实在线莫队在我看来就是个分块,但是不管是预处理的部分还是查询的部分都和莫队十分相似,所以称为莫队也好。

先约定一些东西吧:

令块长为 B,“莫队数组”为莫队需要维护的数组(废话),“莫队状态”指某个状态下的莫队数组及其答案。

大概的过程如下:

  1. 预处理每块的“莫队数组”,在这题也就是每个颜色的出现次数,复杂度为 O(n\frac nB)O(n\frac {n^2} {B^2}),原因一会另讲,这题可以做到 O(n\frac nB)
  2. 预处理块与块间的“莫队状态”的答案,复杂度是 O(n\frac nB)
  3. 整块间贡献直接查,然后用“莫队状态”辅助求出贡献,复杂度 O(mB)

然后就没了,显然这题 B 取到 \frac {n} {\sqrt m} 最优。

在线莫队的空间复杂度通常很劣,会到达 O(n\sqrt n) 甚至更高,实在不行也许可以考虑 unordered_map 并祈祷别卡你(空间和时间都是)。

可以思考一下怎么实现,相信读者一定是会的。

不会做也没关系的说,下面带着代码再来过一遍流程:

for(int i = 1; md[i - 1].pos != n; i ++) {
    int L = B * (i - 1) + 1;
    md[i].pos = std::min(n, B * i);
    for(int j = L; j <= md[i].pos; j ++) lp[j] = i - 1, rp[j] = i; 
    for(int j = 1; j <= md[i].pos; j ++) ++ md[i].cnt[a[j]];
}
然后是 $cnt$,这个就是所讲的“莫队数组”,如果满足可差分性就可以直接相减来求出每个“莫队状态”的这个数组,否则就只能每个状态单独处理,这就是为什么复杂度有时候 $O(n\frac nB)$ 或 $O(n\frac {n^2} {B^2})$。 ```cpp for(int i = 1; md[i - 1].pos != n; i ++) { int ans = 0; for(int j = i + 1; md[j - 1].pos != n; j ++) { for(int k = md[j - 1].pos + 1; k <= md[j].pos; k ++) if(++ cnt[a[k]] == 1) ans ++; mas[i][j] = ans; } memset(cnt, 0, sizeof cnt); } ``` $mas$ 记录的是块间的答案,也就是所谓查询恰好在每块两端的答案,相当于是记录了很多个莫队的状态(所以称之为莫队状态),可以保证不管查询哪一个 $l,r$ 都可以**只移动 $B$ 次指针**而得到结果。 ```cpp int lk = rp[L], rk = lp[R]; l = md[lk].pos, r = md[rk].pos + 1; ans = mas[lk][rk]; for(int i = L; i <= l; i ++) cnt[a[i]] = md[rk].cnt[a[i]] - md[lk].cnt[a[i]]; for(int i = r; i <= R; i ++) cnt[a[i]] = md[rk].cnt[a[i]] - md[lk].cnt[a[i]]; for(int i = L; i <= l; i ++) if(++ cnt[a[i]] == 1) ans ++; for(int i = r; i <= R; i ++) if(++ cnt[a[i]] == 1) ans ++; ``` 这个是查询,$lk,rk$ 代表 $L,R$ 最靠近的块,然后 $l,r$ 则代表莫队的两个指针,接下来四个循环,前两个处理**要用的**莫队数组,后两个模拟莫队指针移动过程,然后就对完了。 这题我是没法给出评测地址什么的了,就把 AC 代码放这了,比主席树短得多。 #### code ```cpp // code by 樓影沫瞬_Hz17 #include <bits/stdc++.h> constexpr int N = 500000 + 10; constexpr int B = 750, T = N / B + 10; struct Moduis { int pos; int cnt[50001]; // 卡空间,没开满 } md[T]; int mas[T][T]; int cnt[50001], a[N]; int lp[N], rp[N]; int n, m; int main() { using std::cin; using std::cout; std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); cin >> n >> m; std::vector<int> v; for(int i = 1; i <= n; i ++) cin >> a[i], v.push_back(a[i]); std::sort(v.begin(), v.end()), v.erase(std::unique(v.begin(), v.end()), v.end()); for(int i = 1; i <= n; i ++) a[i] = std::lower_bound(v.begin(), v.end(), a[i]) - v.begin(); for(int i = 1; md[i - 1].pos != n; i ++) { int L = B * (i - 1) + 1; md[i].pos = std::min(n, B * i); for(int j = L; j <= md[i].pos; j ++) lp[j] = i - 1, rp[j] = i; for(int j = 1; j <= md[i].pos; j ++) ++ md[i].cnt[a[j]]; } for(int i = 1; md[i - 1].pos != n; i ++) { int ans = 0; for(int j = i + 1; md[j - 1].pos != n; j ++) { for(int k = md[j - 1].pos + 1; k <= md[j].pos; k ++) if(++ cnt[a[k]] == 1) ans ++; mas[i][j] = ans; } memset(cnt, 0, sizeof cnt); } for(int c = 1, ans = 0, L, R, l, r; c <= m; c ++) { cin >> L >> R; L = (L + ans) % n + 1, R = (R + ans) % n + 1; if(L > R) std::swap(L, R); if(rp[L] == rp[R]) { ans = 0; for(int i = L; i <= R; i ++) cnt[a[i]] = 0; for(int i = L; i <= R; i ++) if(++ cnt[a[i]] == 1) ans ++; } else { int lk = rp[L], rk = lp[R]; l = md[lk].pos, r = md[rk].pos + 1; ans = mas[lk][rk]; for(int i = L; i <= l; i ++) cnt[a[i]] = md[rk].cnt[a[i]] - md[lk].cnt[a[i]]; for(int i = r; i <= R; i ++) cnt[a[i]] = md[rk].cnt[a[i]] - md[lk].cnt[a[i]]; for(int i = L; i <= l; i ++) if(++ cnt[a[i]] == 1) ans ++; for(int i = r; i <= R; i ++) if(++ cnt[a[i]] == 1) ans ++; } cout << ans << '\n'; } } // 星間~ 干渉~ 融解~ 輪迴~ 邂逅~ 再生~ ララバイ~ ``` ### 例题(?):更疯狂的颜色序列(没这题,乱说的) 注意到,并不存在这个题目。 和上面一样的题目,同样在线,多了一个单点修改。 如果有一个修改,我们的 $cnt$ 和 $mas$ 竟然全线崩溃了!都重构的话岂不是慢完了!!\ 我们发现这个很让人苦恼,难道只能用上高级树形数据结构了吗? 啊,相信大家都可以一瞬间想到怎么办对吧。 $cnt$ 数组只有 $\frac nB$ 个,暴力修改就是对的,而 $mas$ 竟然有 $\frac{n^2}{B^2}$ 个,这很可怕。这里介绍一个摆烂的和一个不摆烂的解决方案。 摆烂点,注意到我们只会用到 $m$ 个 $mas$,直接赖着不改,,存下来,用到了再说。真的用到了就看看这个块的答案有没有被影响,一共检查上次检查到现在的修改了的颜色个数次,这点只要存储修改和每个块的修改时间戳就可以做到,发现最劣是 $O(nm)$ 的,然而真有人卡这个吗?查询到同一个 $mas$ 的概率都不小吧,[何况类似的假复杂度又不少](https://www.luogu.com.cn/discuss/1195628)。 严谨点,我们认为出题人一定会卡(预知了你的块长!!),那么就更改 $B$ 为 $n^\frac23$ 然后暴力修改就解决了呢,简直是比摆烂的方法更加容易……总复杂度是 $O(n^\frac 53)$,和一般的带修莫队一样。 代码写了后一个的,可以通过[【模板】带修莫队](https://www.luogu.com.cn/problem/P1903),这个东西常数巨大(的确可以缩小但是还是会比莫队大)而且我懒得卡了,但是保证正确。 讲解放代码里了,反正总共没两句话。 #### code ```cpp // code by 樓影沫瞬_Hz17 #include <bits/stdc++.h> constexpr int N = 133333 + 10; constexpr int B = 1050, T = N / B + 100, V = 1000000 + 10; struct Moduis { int pos; uint16_t cnt[V]; } md[T]; int mas[T][T]; int cnt[V], a[N]; int lp[N], rp[N]; int n, m; int main() { #ifndef ONLINE_JUDGE freopen("in.ru", "r", stdin); freopen("out.ru", "w", stdout); #endif using std::cin; using std::cout; std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); cin >> n >> m; for(int i = 1; i <= n; i ++) cin >> a[i]; for(int i = 1; md[i - 1].pos != n; i ++) { int L = B * (i - 1) + 1; md[i].pos = std::min(n, B * i); for(int j = L; j <= md[i].pos; j ++) lp[j] = i - 1, rp[j] = i; for(int j = 1; j <= md[i].pos; j ++) ++ md[i].cnt[a[j]]; } for(int i = 1; md[i - 1].pos != n; i ++) { int ans = 0; for(int j = i + 1; md[j - 1].pos != n; j ++) { for(int k = md[j - 1].pos + 1; k <= md[j].pos; k ++) if(++ cnt[a[k]] == 1) ans ++; mas[i][j] = ans; } for(int j = md[i].pos; j <= n; j ++) cnt[a[j]] = 0; } char op; for(int c = 1, ans = 0, L, R, l, r, p, v; c <= m; c ++) { cin >> op; if(op == 'Q') { cin >> L >> R; if(rp[L] == rp[R]) { ans = 0; for(int i = L; i <= R; i ++) cnt[a[i]] = 0; for(int i = L; i <= R; i ++) if(++ cnt[a[i]] == 1) ans ++; } else { int lk = rp[L], rk = lp[R]; l = md[lk].pos, r = md[rk].pos + 1; ans = mas[lk][rk]; for(int i = L; i <= l; i ++) cnt[a[i]] = md[rk].cnt[a[i]] - md[lk].cnt[a[i]]; for(int i = r; i <= R; i ++) cnt[a[i]] = md[rk].cnt[a[i]] - md[lk].cnt[a[i]]; for(int i = L; i <= l; i ++) if(++ cnt[a[i]] == 1) ans ++; for(int i = r; i <= R; i ++) if(++ cnt[a[i]] == 1) ans ++; } cout << ans << '\n'; } else { // 就多了这里,是简单暴力修改 cin >> p >> v; if(a[p] == v) continue; for(int i = 1; md[i - 1].pos != n; i ++) if(md[i].pos >= p) md[i].cnt[a[p]] --, md[i].cnt[v] ++; // 修改数量 for(int i = 1; md[i - 1].pos != n; i ++) for(int j = i + 1; md[j - 1].pos != n; j ++) if(md[i].pos + 1 <= p and p <= md[j].pos) { if(md[j].cnt[v] - md[i].cnt[v] == 1) mas[i][j] ++; if(md[j].cnt[a[p]] - md[i].cnt[a[p]] == 0) mas[i][j] --; // 修改 mas } a[p] = v; } } } // 星間~ 干渉~ 融解~ 輪迴~ 邂逅~ 再生~ ララバイ~ ``` ### 在线莫队和回滚莫队之间不得不说的二三事 我上面写的东西……没错,对应的就已经是不删除莫队,也就是回滚莫队了。 这难道不是十分自然的不删除吗?而且我感觉这孩子的代码难度和回滚莫队差不多来着的。 而且很多回滚莫队的题都会稍微多给一点点的时间,导致如果空间允许其实这个一般不会被卡死,所以结果就是回滚莫队基本上它都可以写写。 这里不给出什么例题和代码了,主要是懒得写,也许未来心情好会补上? ### [例题 P6177【模板】树上在线莫队](https://www.luogu.com.cn/problem/P6177) 树上莫队应该是只能单独维护每个莫队状态的莫队数组,这是因为树上莫队的加减是交替进行的,欧拉序上同一个位置有可能加也有可能减,不符合可差分性。 然后是这题,这是树上链数颜色问题,显然是板,直接树上莫队就行,然后我们再进行在线化。如上所言,只能每两块一个,维护 $\frac {n^2}{B^2}$ 个莫队数组,所以时空复杂度来都到了 $O(nm^\frac 23)$。 > upd:发现预处理莫队数组的块之间的变化较小,而且满足下一个由上一个更新而来,也就是说,这种不符合可差分性的莫队数组,也许可以考虑用可持久化数组(主席树)来做,空间时间都是 $O(n\sqrt m \log n)$。 没办法的事啊,以及如果不用一个重要优化的话会很容易 TLE,下一节放卡常技巧吧,这里直接上加了优化的代码。 更详细的关于此题的细节可以看看[**这个**](https://www.cnblogs.com/lymsHz17/p/19218883)。 #### code ```cpp // code by 樓影沫瞬_Hz17 #include <bits/stdc++.h> constexpr int N = 4e4 + 10, M = 1e5 + 10; struct edge { int to, nxt; } e[N * 2]; int hd[N]; inline void add(int u, int v) { static int tot = 1; e[++ tot] = {v, hd[u]}, hd[u] = tot; e[++ tot] = {u, hd[v]}, hd[v] = tot; } int cne, rr[N * 2], st[N], ed[N]; int dep[N], fa[N], sz[N], wc[N], top[N]; inline void dsf(int u, int f) { dep[u] = dep[f] + 1, fa[u] = f, sz[u] = 1; for(int i = hd[u], v = e[i].to; i; i = e[i].nxt, v = e[i].to) if(v != f) { dsf(v, u); sz[u] += sz[v]; if(sz[wc[u]] < sz[v]) wc[u] = v; } } inline void dfs(int u, int tp) { top[u] = tp, st[u] = ++ cne, rr[cne] = u; if(wc[u]) dfs(wc[u], tp); for(int i = hd[u], v = e[i].to; i; i = e[i].nxt, v = e[i].to) if(v != fa[u] and v != wc[u]) dfs(v, v); ed[u] = ++cne, rr[cne] = u; } inline int lca(int a, int b) { while(top[a] != top[b]) dep[top[a]] > dep[top[b]] ? a = fa[top[a]] : b = fa[top[b]]; return dep[a] > dep[b] ? b : a; } constexpr int B = 1100, T = N * 2 / B + 10; int x[N], a[N], n, m, V; struct Moduis { uint16_t cnt[N]; int ans; bool vis[N]; void change(int p) { (vis[p] ^= 1) ? ans += !cnt[a[p]] ++ : ans -= ! --cnt[a[p]]; } } mas[T][T]; bool vis[N]; int ans; uint16_t cnt[N], pos[N * 2]; int L[T], R[T]; inline void change(int p) { (vis[p] ^= 1) ? ans += !cnt[a[p]] ++ : ans -= !-- cnt[a[p]]; } signed main() { #ifndef ONLINE_JUDGE freopen("in.ru", "r", stdin); freopen("out.ru", "w", stdout); #endif using std::cin; using std::cout; std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); cin >> n >> m; for(int i = 1; i <= n; i ++) cin >> a[i], x[i] = a[i]; for(int i = 1, u, v; i < n; i ++) { cin >> u >> v; add(u, v); } std::sort(&x[1], &x[n] + 1); V = std::unique(&x[1], &x[n] + 1) - x - 1; for(int i = 1; i <= n; i ++) a[i] = std::lower_bound(&x[1], &x[n] + 1, a[i]) - x; dsf(1, 1), dfs(1, 1); for(int i = 1; R[i - 1] != cne; i ++) { L[i] = (i - 1) * B + 1, R[i] = std::min(B * i, cne); for(int j = L[i]; j <= R[i]; j ++) pos[j] = i; } for(int i = 1; R[i - 1] != cne; i ++) { for(int j = i + 1; R[j - 1] != cne; j ++) { for(int k = L[i + 1]; k <= R[j - 1]; k ++) mas[i][j].change(rr[k]); } } for(int c = 1, u, v, l, r, lp, rp, ql, qr; c <= m; c ++) { cin >> u >> v; u ^= ans; if(st[u] > st[v]) std::swap(u, v); int lf = lca(u, v); if(lf == u) l = st[u], r = st[v], lf = 0; else l = ed[u], r = st[v]; ql = pos[l], qr = pos[r]; if(ql == qr) { ans = 0; cnt[a[lf]] = vis[lf] = 0; for(int i = l; i <= r; i ++) cnt[a[rr[i]]] = vis[rr[i]] = 0; for(int i = l; i <= r; i ++) change(rr[i]); if(lf) change(lf); } else { if(l - R[ql - 1] < R[ql] - l and ql != 1) ql --; if(L[qr + 1] - r < r - L[qr] and qr != pos[cne]) qr ++; lp = R[ql], rp = L[qr]; ans = mas[ql][qr].ans; cnt[a[lf]] = mas[ql][qr].cnt[a[lf]]; vis[lf] = mas[ql][qr].vis[lf]; for(int i = l; i <= lp; i ++) cnt[a[rr[i]]] = mas[ql][qr].cnt[a[rr[i]]], vis[rr[i]] = mas[ql][qr].vis[rr[i]]; for(int i = lp + 1; i < l; i ++) cnt[a[rr[i]]] = mas[ql][qr].cnt[a[rr[i]]], vis[rr[i]] = mas[ql][qr].vis[rr[i]]; for(int i = rp; i <= r; i ++) cnt[a[rr[i]]] = mas[ql][qr].cnt[a[rr[i]]], vis[rr[i]] = mas[ql][qr].vis[rr[i]]; for(int i = r + 1; i < rp; i ++) cnt[a[rr[i]]] = mas[ql][qr].cnt[a[rr[i]]], vis[rr[i]] = mas[ql][qr].vis[rr[i]]; for(int i = l; i <= lp; i ++) change(rr[i]); for(int i = lp + 1; i < l; i ++) change(rr[i]); for(int i = rp; i <= r; i ++) change(rr[i]); for(int i = r + 1; i < rp; i ++) change(rr[i]); if(lf) change(lf); } cout << ans << '\n'; } } // 星間~ 干渉~ 融解~ 輪迴~ 邂逅~ 再生~ ララバイ~ ``` ### 常数优化 一个很简单的东西,但是重要程度值得单开一节,毕竟是理论(大概是)$\frac23$ 的常数优化。 其实很简单,发现我们寻找一个离着查询的 $l,r$ 最近的“莫队状态”的过程竟然是随缘找最近的块的左端点或右端点!这很劣,不妨人为找更近的那个。 ## 后记 啊,怎么就结束了? 好吧,最后,感谢观看。