莫队
各种莫队
莫队是啥?是暴力 是根号平衡思想在区间信息问题求解上的集中体现。由莫涛总结提出。
莫队算法的实质是 \(k\) 维空间内 \(n\) 点间的路径规划方式,其路径长度被证明达到了期望下界,是 \(O(n^{\frac {2k - 1} k})\) 的。一种规划方案定义为由原点以单次 \(O(1)\) 的花费向某一维度的正/负方向移动单位长度的方式遍历所有整点的移动方法。最优方案即为总花费渐进最小的方案。
以二维空间下莫队算法为例。
对于二维空间内 \(n\) 个整点 \((x_i, y_i)\),记坐标值域为 \(L\)。我们将证明存在一类路径的构造方法,使得自原点可以依次以期望下最优方案遍历所有整点。
具体地,我们首先将值域分为 \(S\) 块,随后按 \(x\) 坐标所在块为第一关键字、\(y\) 坐标为第二关键字排序,随后按照以此方式构造的序列遍历。
分析复杂度。
\(x\) 坐标位于相同块时,\(y\) 方向的位移为 \(O(L)\)。\(x\) 坐标在块间转移时,\(y\) 方向的位移为 \(O(L)\)。由于相同块共 \(\frac LS\) 个,因此 \(y\) 方向的位移总共为 \(O(L \times \frac L S)\)。
序列中相邻两个整点的 \(x\) 坐标位于相同块时,\(x\) 方向的位移最劣情况是 \(S\) 的。不同时,位移最劣是 \(2S\) 的。由于序列共 \(n\) 个元素,因此 \(x\) 方向的位移总共为 \(O(n S)\)。
因此总路径长度为 \(O(nS + \frac {L^2}S)\)。
可以取 \(S = \sqrt L\),得到复杂度 \(O(n \sqrt L + L \sqrt L)\)。也可以取 \(S = \frac L {\sqrt n}\),得到复杂度 \(O(L \sqrt n)\)。
可以进行外推,当维护 \(k\) 维信息时对前 \(k-1\) 维分大小为 \(S\) 的块,共 \((\frac nS)^{k-1}\) 块。两块间转移的复杂度为 \(n\) ,因此块间位移为 \(\frac {n^k} {S^{k-1}}\)。块内部转移总复杂度是 \(nS\),因此总时间复杂度在 \(S = n^{\frac{k-1}k}\) 时最优,为 \(O(n^{\frac{2k-1}{k}})\)。 - By EntropyIncreaser.
普通莫队
二维整点路径规划问题。
(不)形式化地,给定一个 \(n\) 长度的序列,我们有 \(m\) 个区间,需要查询这些区间内的元素种类。
将区间抽象为二维整点,我们需要的就是在遍历过程中动态维护所有元素的计数器,以及当前答案。当计数器 \(0\rightarrow 1\) 时答案自增;当计数器 \(1\rightarrow 0\) 时答案自减。遍历到整点时将答案记录至整点上即可。
例题:SP3267 D-query on luogu
讨论一下坐标移动问题。由于我们实际上维护的是区间信息,因此需要保证“一个信息被删除前是存在的”。投影到坐标系内,我们需要使得 \(x \le y\)。也即,坐标需要在 \(y = x\) 直线的左上方移动。为此,我们可以先向左上角移动坐标,随后向右下角移动。这样可以避免 \(x > y\) 的情况发生,不会删除不存在的元素。
转换到实现上,我们可以维护 \(l,r\) 指针表示当前覆盖的区间左右端点,随后按 l--, r++, r--, l++
的顺序尝试移动端点到下一个区间范围。
带修莫队
三维整点路径规划问题。
(不)形式化地,给定一个 \(n\) 长度的序列,我们有 \(m\) 个区间,需要查询这些区间内的元素种类。但是这些查询都是按时间顺序发生的,在两个时间片间可能会发生修改。
将区间抽象成三维整点,我们需要的就是在以如上方式遍历的过程中维护时间维信息。我们可以首先维护前两维信息,在移动到 \(x,y\) 坐标与目标点相同位置后,单独处理时间维修改。普通莫队的自然推广,平凡。
例题:P1903 [国家集训队] 数颜色 / 维护队列
具体实现上,我们可以将修改和查询分两个数组记录,在读入过程中将时间维处理掉。当读入一次询问时,其 \(z\) 坐标大小就是已读入修改数量。
树上莫队
使用欧拉序将树拍成序列。
(不)形式化地,给定一棵 \(n\) 个节点的树,我们有 \(m\) 个点对,需要查询两点间路径上的元素种类。
具体地,我们将树上每个节点的欧拉序记作 \((\text{in}_ i, \text{out}_ i)\),分别代表遍历到这个点到离开这个点时的时间戳。并按时间顺序将节点编号排成长度为 \(2n\) 的序列。
讨论点对 \((x,y)\) 的转化。不妨设 \(\text{in}_ x < \text{in}_ y\)。
- \(LCA(x, y) = x\)。此时序列中位于 \([\text{in}_ x , \text{in}_ y]\) 区间内仅出现过一次的节点即为路径上节点。
首先路径上节点都会出现在该区间内,充分性得证。任意出现过两次的节点都是在这条链外的子树,出现过一次的节点都是遍历该段时向下而未向上的节点,即在所求路径上的点,充分性得证。
因此此类点对可被转化为序列上 \([\text{in}_ x , \text{in}_ y]\) 区间的求解。 - \(LCA(x, y) \neq x\)。此时序列中位于 \([\text{out}_ x , \text{in}_ y]\) 区间内仅出现过一次的节点即为路径上节点。
类似1.证明。
因此此类点对可被转化为序列上 \([\text{out}_ x , \text{in}_ y]\) 区间的求解。
因此树上问题被转化为序列问题。注意在本段内可能存在出现两次的点,这些点的贡献不应出现在答案中,可以考虑记录每个编号的出现次数奇偶性,当出现偶次时不处理,实现时 xor 解决。其余同上两种莫队求解方式。
回滚莫队
二次离线莫队
以模板题代码为例。
#include <cmath>
#include <tuple>
#include <vector>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define MXIM 100010
#define ll long long
#define rep(i,a,b) for ( register int (i) = (a); (i) ^ ((b)+1); ++(i) )
#define pre(i,a,b) for ( register int (i) = (a); (i) ^ ((b)-1); --(i) )
int n, m, k, len;
int a[MXIM], cntb[MXIM], pref[MXIM];
ll ans[MXIM];
#define st(block_id) ( n / len * ((block_id)-1) + 1)
#define ed(block_id) ( block_id == len ? n : ( n / len * (block_id) ) )
#define bl(value_pos) ( value_pos == n ? len : ((value_pos) - 1) / len + 1 )
#define sz(block_id) ( ed( (block_id) ) - st( (block_id) ) + 1 )
struct query {
int l, r, id;
ll ans;
inline bool operator < (const query & b) {
return bl(l) == bl(b.l) ? r < b.r : l < b.l;
}
}q[MXIM];
vector<tuple<int, int, int>> vec[MXIM];
int main () {
/* 设点 x 对区间 [l,r] 的贡献是 f(x, [l,r])
* 莫队嘛,肯定得要讨论端点变化带来的影响
* 那么我们看往左边加点的影响
* 即快速地求得 f(x, [l, x-1])
* 于是我们差分一下
* f(x, [l, x-1]) = f(x, [1, x-1]) - f(x, [1, l-1])
* 于是就成要快速地求得 x 对一个前缀的贡献
* 于是我们发现有两种前缀
* 1. f(x, [1, x-1])
一个前缀和它后面一个点对它的贡献
一共 n 个,预处理即可
2. f(x, [1, l-1])
其实我们要求的东西并不是简单的一个 f(x, [l, x-1])
而是 x 对 [l, r] -> [l, r+k] 的贡献
具体点说就是
任意 x \in [r+1, r+k], f(x, [l, x-1])
换到2.前缀上来说,就是 x \in [r+1, r+k], f(x, [1, l])
我们会发现一个性质,就是,x点变了,但它所贡献的区间不变
于是打一下x移动左右端点的标记,再莫一遍[1,l]所得到的贡献,于是二离计算
*/
cin >> n >> m >> k;
if (k > 14) {
rep(i,1,m) cout << 0 << endl;
return 0;
} len = sqrt(n);
rep(i,1,n) cin >> a[i];
rep(i,1,m) cin >> q[i].l >> q[i].r, q[i].id = i;
sort(q+1, q+1+m);
vector<int> buk;
rep(i,0,16383) if (__builtin_popcount(i) == k) buk.emplace_back(i);
rep(i,1,n) {
for (auto x : buk) ++cntb[a[i] ^ x]; // 看前缀里和 i 异或得 k 的数的数量
pref[i] = cntb[a[i+1]]; // 讨论 x 对 [1, x-1] 的贡献
} memset(cntb, 0, sizeof cntb);
for (register int i = 1, L = 1, R = 0, l, r; i <= m; ++i) {
// 我莫队一下
// 1.前缀就出来了,我再给2.前缀打好标记
l = q[i].l, r = q[i].r;
if (L < l) {
vec[R].emplace_back(L, l-1, -i);
while (L < l) q[i].ans += pref[(L++) - 1];
}
if (L > l) {
vec[R].emplace_back(l, L-1, i);
while (L > l) q[i].ans -= pref[(L--) - 2];
}
if (R < r) {
vec[L-1].emplace_back(R+1, r, -i);
while (R < r) q[i].ans += pref[(R++)];
}
if (R > r) {
vec[L-1].emplace_back(r+1, R, i);
while (R > r) q[i].ans -= pref[(R--) - 1];
}
}
for(register int i = 1, id, l, r; i <= n; ++i) {
/* 计算2.前缀
* 我们既然有贡献区间不变,那就直接把 x 的贡献搞出来
* 给对应前缀区间加上就行
*/
for (auto x : buk) ++cntb[a[i] ^ x];
for (auto x : vec[i]) {
tie(l, r, id) = x;
// tie(arg ...) = [tuple] 使得前面tie里提供的变量被依次赋值为这个 tuple 里面的元素
for (register int j = l, tmp = 0; j <= r; ++j) {
tmp = cntb[a[j]];
// 这是 x = j 时的贡献
if (j <= i and k == 0) --tmp;
if (id < 0) q[-id].ans -= tmp;
// id 存着 得到贡献的前缀区间
else q[id].ans += tmp;
}
}
}
rep(i,1,m) q[i].ans += q[i-1].ans;
// 既然是前缀区间 那就前缀和
rep(i,1,m) ans[q[i].id] = q[i].ans;
rep(i,1,m) cout << ans[i] << endl;
return 0;
}
待补
以下是博客签名,与正文无关。
请按如下方式引用此页:
本文作者 joke3579,原文链接:https://www.cnblogs.com/joke3579/p/mo_s_queue.html。
遵循 CC BY-NC-SA 4.0 协议。
请读者尽量不要在评论区发布与博客内文完全无关的评论,视情况可能删除。