莫队
各种莫队
莫队是啥?是暴力 是根号平衡思想在区间信息问题求解上的集中体现。由莫涛总结提出。
莫队算法的实质是 维空间内 点间的路径规划方式,其路径长度被证明达到了期望下界,是 的。一种规划方案定义为由原点以单次 的花费向某一维度的正/负方向移动单位长度的方式遍历所有整点的移动方法。最优方案即为总花费渐进最小的方案。
以二维空间下莫队算法为例。
对于二维空间内 个整点 ,记坐标值域为 。我们将证明存在一类路径的构造方法,使得自原点可以依次以期望下最优方案遍历所有整点。
具体地,我们首先将值域分为 块,随后按 坐标所在块为第一关键字、 坐标为第二关键字排序,随后按照以此方式构造的序列遍历。
分析复杂度。
坐标位于相同块时, 方向的位移为 。 坐标在块间转移时, 方向的位移为 。由于相同块共 个,因此 方向的位移总共为 。
序列中相邻两个整点的 坐标位于相同块时, 方向的位移最劣情况是 的。不同时,位移最劣是 的。由于序列共 个元素,因此 方向的位移总共为 。
因此总路径长度为 。
可以取 ,得到复杂度 。也可以取 ,得到复杂度 。
可以进行外推,当维护 维信息时对前 维分大小为 的块,共 块。两块间转移的复杂度为 ,因此块间位移为 。块内部转移总复杂度是 ,因此总时间复杂度在 时最优,为 。 - By EntropyIncreaser.
普通莫队
二维整点路径规划问题。
(不)形式化地,给定一个 长度的序列,我们有 个区间,需要查询这些区间内的元素种类。
将区间抽象为二维整点,我们需要的就是在遍历过程中动态维护所有元素的计数器,以及当前答案。当计数器 时答案自增;当计数器 时答案自减。遍历到整点时将答案记录至整点上即可。
例题:SP3267 D-query on luogu
讨论一下坐标移动问题。由于我们实际上维护的是区间信息,因此需要保证“一个信息被删除前是存在的”。投影到坐标系内,我们需要使得 。也即,坐标需要在 直线的左上方移动。为此,我们可以先向左上角移动坐标,随后向右下角移动。这样可以避免 的情况发生,不会删除不存在的元素。
转换到实现上,我们可以维护 指针表示当前覆盖的区间左右端点,随后按 l--, r++, r--, l++
的顺序尝试移动端点到下一个区间范围。
带修莫队
三维整点路径规划问题。
(不)形式化地,给定一个 长度的序列,我们有 个区间,需要查询这些区间内的元素种类。但是这些查询都是按时间顺序发生的,在两个时间片间可能会发生修改。
将区间抽象成三维整点,我们需要的就是在以如上方式遍历的过程中维护时间维信息。我们可以首先维护前两维信息,在移动到 坐标与目标点相同位置后,单独处理时间维修改。普通莫队的自然推广,平凡。
例题:P1903 [国家集训队] 数颜色 / 维护队列
具体实现上,我们可以将修改和查询分两个数组记录,在读入过程中将时间维处理掉。当读入一次询问时,其 坐标大小就是已读入修改数量。
树上莫队
使用欧拉序将树拍成序列。
(不)形式化地,给定一棵 个节点的树,我们有 个点对,需要查询两点间路径上的元素种类。
具体地,我们将树上每个节点的欧拉序记作 ,分别代表遍历到这个点到离开这个点时的时间戳。并按时间顺序将节点编号排成长度为 的序列。
讨论点对 的转化。不妨设 。
- 。此时序列中位于 区间内仅出现过一次的节点即为路径上节点。
首先路径上节点都会出现在该区间内,充分性得证。任意出现过两次的节点都是在这条链外的子树,出现过一次的节点都是遍历该段时向下而未向上的节点,即在所求路径上的点,充分性得证。
因此此类点对可被转化为序列上 区间的求解。 - 。此时序列中位于 区间内仅出现过一次的节点即为路径上节点。
类似1.证明。
因此此类点对可被转化为序列上 区间的求解。
因此树上问题被转化为序列问题。注意在本段内可能存在出现两次的点,这些点的贡献不应出现在答案中,可以考虑记录每个编号的出现次数奇偶性,当出现偶次时不处理,实现时 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 协议。
请读者尽量不要在评论区发布与博客内文完全无关的评论,视情况可能删除。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)