在线莫队学习笔记
lyms_Hz17
·
·
算法·理论
前言
(似乎叫诗乃莫队)
其实刚学莫队的时候就听说这个了,一直想学来着,在学了主席树后更想学了,然后……
一直到现在我都没学,但是很好造,直接造出来了,你来你也行。
下文分别是几个经典莫队的在线化改造。分别是莫队,带修莫队,回滚莫队,树上莫队。最后是一点点的卡常。
然后前两篇代码因为还处于摸索阶段所以码比较丑,最后一篇代码我还是比较满意的。
正文
先来按着我的理解概述一下在线莫队吧,是一个编码容易但是编码较长,常数较大但是常数好卡的东西。唯一较大的缺陷就是空间好劣……
例题:疯狂的颜色序列
一句话题意:区间数颜色,在线。
这个是出现在我们学校题库主席树专题里的题目,当时就非常想写莫队,可惜啊……
不过现在也来得及。
说是莫队,其实在线莫队在我看来就是个分块,但是不管是预处理的部分还是查询的部分都和莫队十分相似,所以称为莫队也好。
先约定一些东西吧:
令块长为 B,“莫队数组”为莫队需要维护的数组(废话),“莫队状态”指某个状态下的莫队数组及其答案。
大概的过程如下:
- 预处理每块的“莫队数组”,在这题也就是每个颜色的出现次数,复杂度为 O(n\frac nB) 或 O(n\frac {n^2} {B^2}),原因一会另讲,这题可以做到 O(n\frac nB)。
- 预处理块与块间的“莫队状态”的答案,复杂度是 O(n\frac nB)。
- 整块间贡献直接查,然后用“莫队状态”辅助求出贡献,复杂度 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$ 最近的“莫队状态”的过程竟然是随缘找最近的块的左端点或右端点!这很劣,不妨人为找更近的那个。
## 后记
啊,怎么就结束了?
好吧,最后,感谢观看。