莫队学习笔记(2)回滚莫队
普通莫队在左右端点移动的过程中,当加点和删点操作其中一个很容易实现,但是另一个操作不易实现时,我们可以通过特殊方法实现只增加不删除或只删除不增加,这种莫队被称为回滚莫队,以只增加不删除的回滚莫队为例:
将左端点按照所在块的编号为第一关键字升序排序,右端点按照所在的位置为第二关键字升序排序,处理每次询问前都必须要保证左端点在当前询问的左端点所在块的末尾,从而保证只增加不删除。
首先,若当前和上次询问的左端点所在块不同,则将左右端点都移至左端点所在块的末尾。
其次,若当前询问的左右端点都处于同一个块内,则直接暴力求解答案即可。
否则,先将右端点扩展到当前询问的右端点处,然后记下当前的答案,之后再将左端点向左扩展到当前询问的左端点求出答案,最后将左端点回滚到当前询问的左端点所在块的末尾,并将之前左端点向左扩展所修改的信息都还原回去,答案改为之前记下的答案。
不难发现,这样就保证了只增加不删除,并且与普通莫队相比时间复杂度仍为 $O(n\sqrt{q})$,常数也只大了两倍。
对于只删除不增加的回滚莫队,也是类似的方法,只需要略微修改一下即可——右端点按照所在的位置为第二关键字降序排序,处理每次询问前都必须要保证左端点在当前询问的左端点所在块的开头。
- 例题 1:AT1219 歴史の研究
题意:给定长为 $n$ 的序列和 $q$ 次询问,每次询问区间带权最大众数(数值与出现次数乘积最大),$1\le n,q\le10^5$,时限 4s。
用普通莫队去做比较难,原因就在于增加时很好维护答案,但是删除时不易维护答案,因此采用只增加不删除的回滚莫队即可。
1 const int N = 1e5 + 10; 2 int n, Q, B, k, a[N], b[N], pos[N], R[N], cnt[N], tmpcnt[N]; 3 ll ans, Ans[N]; 4 struct query 5 { 6 int l, r, id; 7 inline bool operator < (const query &x) const 8 { 9 return pos[l] != pos[x.l] ? pos[l] < pos[x.l] : r < x.r; 10 } 11 } q[N]; 12 13 inline void add(int x) { ans = Max(ans, 1ll * b[x] * (++cnt[x])); } 14 inline void del(int x) { --cnt[x]; } 15 16 int main() 17 { 18 read(n, Q); B = ceil(sqrt(3) * n / sqrt(Q)); 19 for (int i = 1; i <= n; ++i) read(a[i]), b[i] = a[i]; 20 sort(b + 1, b + n + 1); k = unique(b + 1, b + n + 1) - b - 1; 21 for (int i = 1; i <= n; ++i) a[i] = lower_bound(b + 1, b + k + 1, a[i]) - b; 22 for (int i = 1; i <= n; ++i) pos[i] = (i - 1) / B + 1, R[i] = pos[i] * B; 23 for (int i = 1; i <= Q; ++i) read(q[i].l, q[i].r), q[i].id = i; 24 sort(q + 1, q + Q + 1); 25 for (int l = 1, r = 0, laspos = 0, i = 1; i <= Q; ++i) 26 { 27 if (pos[q[i].l] == pos[q[i].r]) 28 { 29 for (int j = q[i].l; j <= q[i].r; ++j) 30 Ans[q[i].id] = Max(Ans[q[i].id], 1ll * b[a[j]] * (++tmpcnt[a[j]])); 31 for (int j = q[i].l; j <= q[i].r; ++j) tmpcnt[a[j]] = 0; 32 continue; 33 } 34 if (pos[q[i].l] != laspos) 35 { 36 while (l <= R[q[i].l]) del(a[l++]); 37 while (r > R[q[i].l]) del(a[r--]); 38 ans = 0, laspos = pos[q[i].l]; 39 } 40 while (r < q[i].r) add(a[++r]); ll tmp = ans; 41 while (l > q[i].l) add(a[--l]); Ans[q[i].id] = ans, ans = tmp; 42 while (l <= R[q[i].l]) del(a[l++]); 43 } 44 for (int i = 1; i <= Q; ++i) print(Ans[i]), pc('\n'); 45 fwrite(pbuf, 1, pp - pbuf, stdout); return 0; 46 }
题意:给定长为 $n$ 的序列和 $m$ 次询问,每次询问区间 $\operatorname{mex}$,$1\le n,m\le2\times10^5$,时限 1s。
由于是 $\operatorname{mex}$ 是没有出现过的,删除的时候可以顺带维护答案,所以采用只删除不增加的回滚莫队即可。
1 const int N = 2e5 + 10; 2 int n, m, B, ans, a[N], pos[N], L[N], cnt[N], tmpcnt[N], Ans[N]; 3 struct query 4 { 5 int l, r, id; 6 inline bool operator < (const query &x) const 7 { 8 return pos[l] != pos[x.l] ? pos[l] < pos[x.l] : r > x.r; 9 } 10 } q[N]; 11 12 inline void add(int x) { ++cnt[x]; } 13 inline void del(int x) { --cnt[x]; if (!cnt[x]) ans = Min(ans, x); } 14 15 int main() 16 { 17 read(n, m); B = ceil(sqrt(3) * n / sqrt(m)); 18 for (int i = 1; i <= n; ++i) 19 read(a[i]), pos[i] = (i - 1) / B + 1, L[i] = (pos[i] - 1) * B + 1; 20 for (int i = 1; i <= n; ++i) ++cnt[a[i]]; 21 int Tmp = 0; while (cnt[Tmp]) ++Tmp; 22 for (int i = 1; i <= m; ++i) read(q[i].l, q[i].r), q[i].id = i, Ans[i] = n; 23 sort(q + 1, q + m + 1); 24 for (int l = 1, r = n, laspos = 0, i = 1; i <= m; ++i) 25 { 26 if (pos[q[i].l] == pos[q[i].r]) 27 { 28 for (int j = q[i].l; j <= q[i].r; ++j) ++tmpcnt[a[j]]; 29 int j = 0; while (tmpcnt[j]) ++j; Ans[q[i].id] = j; 30 for (int j = q[i].l; j <= q[i].r; ++j) --tmpcnt[a[j]]; 31 continue; 32 } 33 if (pos[q[i].l] != laspos) 34 { 35 while (r < n) add(a[++r]); ans = Tmp; 36 while (l < L[q[i].l]) del(a[l++]); 37 Tmp = ans, laspos = pos[q[i].l]; 38 } 39 while (r > q[i].r) del(a[r--]); ll tmp = ans; 40 while (l < q[i].l) del(a[l++]); Ans[q[i].id] = ans, ans = tmp; 41 while (l > L[q[i].l]) add(a[--l]); 42 } 43 for (int i = 1; i <= m; ++i) print(Ans[i]), pc('\n'); 44 fwrite(pbuf, 1, pp - pbuf, stdout); return 0; 45 }
- 例题 3:P5906 【模板】回滚莫队&不删除莫队(双倍经验:SP20644 ZQUERY - Zero Query)
题意:给定长为 $n$ 的序列和 $m$ 次询问,每次询问区间相同的数的最远距离,$1\le n,m\le2\times10^5$,时限 1s。
显然需要维护每个数的一些最值,不难发现只增加很容易更新答案,对于最值问题只能暴力把遇到的值进行储存和还原。
双倍经验题考虑前缀和,用差分思想转化问题,不难发现题意一模一样。
1 const int N = 2e5 + 10; 2 int n, m, B, k, top, ans, a[N], b[N], pos[N], End[N], L[N], R[N], sta[N], Ans[N]; 3 struct query 4 { 5 int l, r, id; 6 inline bool operator < (const query &x) const 7 { 8 return pos[l] != pos[x.l] ? pos[l] < pos[x.l] : r < x.r; 9 } 10 } q[N]; 11 12 int main() 13 { 14 read(n); for (int i = 1; i <= n; ++i) read(a[i]), b[i] = a[i]; 15 sort(b + 1, b + n + 1); k = unique(b + 1, b + n + 1) - b - 1; 16 for (int i = 1; i <= n; ++i) a[i] = lower_bound(b + 1, b + k + 1, a[i]) - b; 17 read(m); B = ceil(sqrt(3) * n / sqrt(m)); ans = 0; 18 for (int i = 1; i <= n; ++i) pos[i] = (i - 1) / B + 1, End[i] = pos[i] * B; 19 for (int i = 1; i <= m; ++i) read(q[i].l, q[i].r), q[i].id = i; 20 sort(q + 1, q + m + 1); 21 for (int l = 1, r = 0, i = 1; i <= m; ++i) 22 { 23 if (pos[q[i].l] != pos[q[i - 1].l]) 24 { 25 while (top) L[a[sta[top]]] = R[a[sta[top]]] = 0, --top; 26 l = End[q[i].l] + 1, r = End[q[i].l], ans = 0; 27 } 28 if (pos[q[i].l] == pos[q[i].r]) 29 { 30 for (int j = q[i].l; j <= q[i].r; ++j) 31 !L[a[j]] ? L[a[j]] = j : Ans[q[i].id] = Max(Ans[q[i].id], j - L[a[j]]); 32 for (int j = q[i].l; j <= q[i].r; ++j) L[a[j]] = 0; 33 continue; 34 } 35 while (r < q[i].r) 36 { 37 ++r, R[a[r]] = r; 38 !L[a[r]] ? sta[++top] = L[a[r]] = r : ans = Max(ans, r - L[a[r]]); 39 } 40 int tmp = ans; 41 while (l > q[i].l) !R[a[--l]] ? R[a[l]] = l : ans = Max(ans, R[a[l]] - l); 42 Ans[q[i].id] = ans, ans = tmp; 43 while (l <= End[q[i].l]) { if (R[a[l]] == l) R[a[l]] = 0; ++l; } 44 } 45 for (int i = 1; i <= m; ++i) print(Ans[i]), pc('\n'); 46 fwrite(pbuf, 1, pp - pbuf, stdout); return 0; 47 }
题意:给定长为 $n$ 的序列和 $m$ 次询问,每次询问区间相同的数的最近距离,$1\le n,m\le5\times10^5$,时限 3s。
与例题 3 所求恰好相反,但思路也是类似的,还是只增加易做。
但是由于这题正解不是回滚莫队,所以出题人卡回滚莫队,因此需要疯狂卡常才能过,代码最慢的点(#29)跑了 2.45s……
1 const int N = 5e5 + 10; 2 int n, m, B, k, top, ans; 3 int a[N], b[N], pos[N], R[N], las[N], fir[N], tmplas[N], sta[N], Ans[N]; 4 struct query 5 { 6 int l, r, id; 7 inline bool operator < (const query &x) const 8 { 9 return pos[l] != pos[x.l] ? pos[l] < pos[x.l] : r < x.r; 10 } 11 } q[N]; 12 13 int main() 14 { 15 read(n, m); B = ceil(sqrt(3) * n / sqrt(m)); 16 for (int i = 1; i <= n; ++i) read(a[i]), b[i] = a[i]; 17 sort(b + 1, b + n + 1); k = unique(b + 1, b + n + 1) - b - 1; 18 for (int i = 1; i <= n; ++i) a[i] = lower_bound(b + 1, b + k + 1, a[i]) - b; 19 for (int i = 1; i <= n; ++i) pos[i] = (i - 1) / B + 1, R[i] = pos[i] * B; 20 for (int i = 1; i <= m; ++i) read(q[i].l, q[i].r), q[i].id = i, Ans[i] = N; 21 sort(q + 1, q + m + 1); 22 for (int l = 1, r = 0, i = 1; i <= m; ++i) 23 { 24 if (pos[q[i].l] != pos[q[i - 1].l]) 25 { 26 while (top) las[sta[top]] = fir[sta[top]] = 0, --top; 27 l = R[q[i].l] + 1, r = R[q[i].l], ans = N; 28 } 29 if (pos[q[i].l] == pos[q[i].r]) 30 { 31 for (int j = q[i].l; j <= q[i].r; ++j) 32 { 33 if (las[a[j]]) Ans[q[i].id] = Min(Ans[q[i].id], j - las[a[j]]); 34 las[a[j]] = j; 35 } 36 for (int j = q[i].l; j <= q[i].r; ++j) las[a[j]] = 0; 37 continue; 38 } 39 while (r < q[i].r) 40 { 41 if (!fir[a[++r]]) fir[a[r]] = r, sta[++top] = a[r]; 42 if (las[a[r]]) ans = Min(ans, r - las[a[r]]); las[a[r]] = r; 43 } 44 int tmp = ans; 45 while (l > q[i].l) 46 if (!tmplas[a[--l]]) 47 { 48 if (fir[a[l]]) ans = Min(ans, fir[a[l]] - l); 49 tmplas[a[l]] = l; 50 } 51 else ans = Min(ans, tmplas[a[l]] - l), tmplas[a[l]] = l; 52 Ans[q[i].id] = ans, ans = tmp; 53 while (l <= R[q[i].l]) tmplas[a[l++]] = 0; 54 } 55 for (int i = 1; i <= m; ++i) Ans[i] >= n ? print(-1) : print(Ans[i]), pc('\n'); 56 fwrite(pbuf, 1, pp - pbuf, stdout); return 0; 57 }
- 例题 5:P8078 [WC2022] 秃子酋长
题意:给定长为 $n$ 的排列和 $m$ 次询问,求区间内排序后相邻的数在原序列中的距离之和,$1\le n,m\le5\times10^5$,时限 5s。
假设排好序了,删除操作用链表维护即可,但是增加操作至少需要一个 $\log$,所以考虑只删除,然后就做完了。
这题写回滚莫队至少就 Ag 了,然而我当时不会回滚莫队只写了个 50pts 的普通莫队+平衡树暴力,所以 Cu 了真的是输麻了……
都过一个月了 CCF 还不公开数据太离谱了……还好热心网友造了 民间数据 link,不过这数据有点卡回滚莫队,最慢的点 4.42s……
1 const int N = 5e5 + 10; 2 int n, m, B, top, a[N], p[N], pos[N], L[N], pre[N], suf[N]; 3 ll ans, Ans[N]; 4 struct node { int a, pre, suf; } sta[N]; 5 struct query 6 { 7 int l, r, id; 8 inline bool operator < (const query &x) const 9 { 10 return pos[l] != pos[x.l] ? pos[l] < pos[x.l] : r > x.r; 11 } 12 } q[N]; 13 14 inline int Abs(int x) { return x > 0 ? x : -x; } 15 inline void del(int x) 16 { 17 if (pre[x]) ans -= Abs(p[x] - p[pre[x]]), suf[pre[x]] = suf[x]; 18 if (suf[x]) ans -= Abs(p[x] - p[suf[x]]), pre[suf[x]] = pre[x]; 19 if (pre[x] && suf[x]) ans += Abs(p[pre[x]] - p[suf[x]]); 20 } 21 inline void add(node x) 22 { 23 pre[x.a] = x.pre, suf[x.pre] = x.a; 24 suf[x.a] = x.suf, pre[x.suf] = x.a; 25 } 26 27 int main() 28 { 29 read(n, m); B = ceil(sqrt(3) * n / sqrt(m)); 30 for (int i = 1; i <= n; ++i) 31 { 32 read(a[i]), p[a[i]] = i, pre[i] = i - 1, suf[i] = i + 1; 33 pos[i] = (i - 1) / B + 1, L[i] = (pos[i] - 1) * B + 1; 34 } 35 suf[n] = 0; 36 for (int i = 2; i <= n; ++i) ans += Abs(p[i] - p[i - 1]); 37 for (int i = 1; i <= m; ++i) read(q[i].l, q[i].r), q[i].id = i; 38 sort(q + 1, q + m + 1); ll Tmp = ans; 39 for (int l = 1, r = n, i = 1; i <= m; ++i) 40 { 41 if (pos[q[i].l] != pos[q[i - 1].l]) 42 { 43 while (r < n) ++r, add(sta[top--]); ans = Tmp; 44 while (l < L[q[i].l]) del(a[l++]); Tmp = ans; 45 } 46 while (r > q[i].r) 47 sta[++top] = (node){ a[r], pre[a[r]], suf[a[r]] }, del(a[r--]); 48 ll tmp = ans; 49 while (l < q[i].l) 50 sta[++top] = (node){ a[l], pre[a[l]], suf[a[l]] }, del(a[l++]); 51 Ans[q[i].id] = ans, ans = tmp; 52 while (l > L[q[i].l]) --l, add(sta[top--]); 53 } 54 for (int i = 1; i <= m; ++i) print(Ans[i]), pc('\n'); 55 fwrite(pbuf, 1, pp - pbuf, stdout); return 0; 56 }
- 例题 6:P5386 [Cnoi2019]数字游戏
题意:给定长为 $n$ 的排列和 $q$ 次询问,求区间内有多少子区间满足最小值 $\ge x$ 且最大值 $\le y$,$1\le n,q\le2\times10^5$,时限 7s。
非常妙的一道题,题目相当于是求极长连续段,但是这个连续段是在序列上的而不是在值域上的,因此不能值域分块去做。
所以考虑将莫队和分块维护的东西换一下,改成在序列上分块,在值域上莫队,这个题就显然能做了。
题意:给定长为 $n$ 的序列和 $m$ 次操作,单点修改或者询问区间内有多少子区间最大值 $\le x$,$1\le n,m\le3\times10^5$,时限 3.5s。
To be continued...