浅谈莫队

浅谈莫队

update on 2022.07.01

昨天被 hs 嘲讽了 /kk

补上了高维莫队

前言

都到 2022 年了,随着莫队二次离线的学习完毕,终于把莫队给学完了!/ll

本文重在总结,而不在证明时间复杂度之类的

至于莫队的时间复杂度为什么带根号,是因为莫队的本质是分块

英文名字都是我蒙的,如果雷人那也么办法

前提

莫队是有适用条件的:

  1. 可离线
  2. \(\mathcal O(1)\)or 删(还有一种情况可以用二次离线解决)
  3. 可接受 \(\mathcal O(n \sqrt m) / \mathcal O(n^{\frac 5 3})\) 的时间复杂度

普通莫队(\(\texttt{Ordinary Mo's Algorithm}\)

最基本的莫队,块长一般设为 \(\frac n {\sqrt m}\)

询问按奇偶排序

inline bool operator < (const query x, const query y)
{
	if (x.lt != y.lt) return x.lt < y.lt;
    return x.lt & 1 ? x.r < y.r : x.r > y.r; // 这一行交换 '<' 和 '>' 也可以
}

记住 \(l,r\) 指针的移动顺序,先扩展再收缩,扩展时先动指针再更新,收缩时先贡献再动指针(这样准没错)

l = 1, r = 0;
for : Queries
{
	while (l > ql) add(--l);
	while (r < qr) add(++r);
	while (l < ql) del(l++);
	while (r > qr) del(r--);   
}

时间复杂度为 \(\mathcal O(n \sqrt m)\)

花神的嘲讽计划Ⅰ

带修改的莫队(\(\texttt{Mo's Algorithm with Modifications}\)

块长一般为 \(n^{\frac 2 3}\)

在询问中加入一维,为上次修改的时间

按左端点所在块、右端点所在块、时间,依次为关键字排序

inline bool operator < (const query x, const query y)
{
    if (x.lt != y.lt) return x.lt < y.lt;
    if (x.rt != y.rt) return x.rt < y.rt;
    return x.t < y.t;
}

当然,也可以加一点玄学奇偶优化

指针也理所应当地再加一个表示时间,同样先扩展再收缩(类比 \(r\)(时间正向流逝应该是一种扩展吧)

但要注意的是,时间的指针应当最后移动

具体地,修改操作进行时应当判断对当前询问的区间是否会产生影响

如果有影响就增加贡献;否则直接改变原序列(默认修改操作形如将 \(a_x\) 改为 \(y\)

而修改操作的撤销,可以理解为再次修改为原值,所以修改时直接 swap(a[x], y)

l = 1, r = 0, t = 0;
for : Queries
{
    while (l > q[i].l) add(--l);
    while (r < q[i].r) add(++r);
    while (l < q[i].l) del(l++);
    while (r > q[i].r) del(r--);
    while (t < q[i].t) change(++t, i); // 执行对应的修改操作,传 i 是为了判断修改位置是否在当前询问区间内
    while (t > q[i].t) change(t--, i);
}

时间复杂度为 \(\mathcal O(n^{\frac 5 3})\)

[国家集训队] 数颜色 / 维护队列

树上莫队(\(\texttt{Mo's Algorithm on Tree}\)

块长一般为 \(\frac {2n} {\sqrt {m}}\)

直接树链剖分遍历树,整出欧拉序(也叫括号序),树上两点间路径的询问转化为欧拉序上对应区间的询问

在区间中出现两次的点,不会出现在路径中,而出现一次的点必然出现在路径中

但出现在路径中的点,不一定会在对应的欧拉序区间中出现(比如两点的 \(\rm LCA\)

所以每次指针移动时需根据该点出现的次数来判断是 add 还是 del

并且还要注意特判 \(\rm LCA\) ,当 \(\rm LCA\) 不为端点时,要在最后加入 \(\rm LCA\) 的贡献

由于欧拉序的长度为 \(2n\),所以多个 \(2\) 的常数

时间复杂度为 \(\mathcal O(2n\sqrt m)\)

COT2 - Count on a tree II

树上带修改的莫队(\(\texttt{Mo's Algorithm with Modification on Tree}\)

额。。。结合上面两种就行了(单纯只是想蒙个英文)

但要注意,在修改进行前,要先算 \(\rm LCA\) 的贡献

块长一般为 \((2n)^{\frac 2 3}\)

时间复杂度为 \(\mathcal O((2n)^{\frac 5 3})\)

[WC2013] 糖果公园

回滚莫队(\(\texttt{Mo's Algorithm Which Can Roll Back}\)

当贡献只能快速地进行 添加/删除 其中一种时,我们还是可以用莫队

块长一般为 \(\sqrt n\)

下面以只加不删莫队为例

按左端点所在块为第一关键字,右端点下标为第二关键字排序

inline bool operator < (const query x, const query y)
{
	return x.lt == y.lt ? x.r < y.r : x.lt < y.lt;
}

枚举每个块 \(i\) ,将 \(l \leftarrow R_i + 1, r \leftarrow R_i\) ,其中 \(R_i\) 是第 \(i\) 块的右端点

枚举左端点在该块内的所有询问:

  • 对于区间长度小于块长的询问,直接暴力做,比如交给函数 inline void yrl_can_do_this(const int l, const int r);
  • 对于区间长度大于块长的询问,\(qr\) 必然大于 \(R_i\),又由于这些询问的右端点 \(qr\) 是递增的,所以右指针 \(r\) 只会 \(r++\),而至于做左指针,处理每个询问时都把 \(l \leftarrow R_i + 1\),并记录此时的状态(就是维护的那些东西),显然 \(l\) 只会 \(l--\) ,最后统计答案后把状态还原。这一过程中维护的区间一直在扩展,所以不需要删除操作。

时间复杂度是 \(\mathcal O(m \sqrt n)\),这里与普通莫队有点出入

所以有些时候可以用这个听上去更高级的玩意儿去替代普通莫队

Rmq Problem / mex

莫队二次离线(\(\texttt{Mo's Algorithm with Twice Offline Invented by lxl}\)

当添加和删除都不是很快时,我们还是可以用莫队(但这当然是有前提的)

莫队是离线算法,但是它还可以离线

块长一般为 \(\sqrt n\)

排序依旧按左端点所在块为第一关键字,右端点下标为第二关键字排序

\(f(x, l, r)\) 表示 \(x\) 这个位置给 \([l, r]\) 这个区间带来的贡献

以右指针 \(r++\) 时为例,新增的贡献 \(\Delta = f(r, l, r - 1)\)

如果恰好这个 \(f\) 可以差分的话(比如 \(a_x\)\(a_{l\sim r}\) 形成的逆序对个数可等于 \(a_x\)\(a_{1 \sim r}\) 形成的逆序对个数,减去 \(a_x\)\(a_{1, l - 1}\) 形成的逆序对个数)

那么有

\[\begin{aligned} \Delta &= f(r, l, r - 1)\\ & =f(r, 1, r - 1) - f(r, 1, l - 1)\\ \end{aligned} \]

如果又恰好 \(f(r, 1, r - 1)\) 可以以可观的时间预处理(比如 \(a_r\)\(a_{1 \sim r - 1}\) 形成的逆序对数可以用树状数组 \(\mathcal O (n \log n)\) 的时间复杂度预处理)

那么我们就可以离线预处理 \(f(r, 1, r - 1)\) (这不废话)

而对于 \(-f(r, 1, l - 1)\) 可以将 \(r\) 插入到 \(l - 1\)vector 中,相当于在从 \(1\) 扫到 \(l - 1\) 时,一次询问 \(a_r\) 与当前扫过的数的逆序对数

在插入 vector 的同时,带上预处理的第一部分的贡献,即 \(f(r, 1, r - 1)\)

然后做一次扫描线,算第二部分的贡献,即 \(-f(r, 1, l - 1)\)

具体地,当扫描到 \(l - 1\) 时,我们算上 \(a_r\) 与当前 \(a_{1 \sim l - 1}\) 所产生的贡献(逆序对数)

这样我们每扫到一个点修改一次,然后处理该点 vector 内的询问

\(\mathcal O(n)\) 次修改,\(\mathcal O(n \sqrt m)\) 次询问(莫队指针移动次数为 \(\mathcal O(n \sqrt m)\) ,每移动一次指针对应一次询问)

有时候如果单次询问时间复杂度还是很高,可以考虑根号平衡,让每次修改 \(\mathcal O(\sqrt n)\) ,每次询问 \(\mathcal O (1)\)

这样时间复杂度 \(\mathcal O(n \sqrt n + n \sqrt m)\) ,比较优秀,但空间复杂度为 \(\mathcal O(n \sqrt m)\),不够优秀 /ng

考虑到右指针 \(r\) 会一直 \(r ++\) 直到不满足 \(r < qr\),所以我们直接存指针的移动,即将 \(\{r, qr - 1\}\) 插入到 \(l - 1\)vector 中,扫到 \(l - 1\) 时再遍历一遍 \(r \rightarrow qr - 1\) 即可,计算方式同上

时间复杂度依旧为 \(\mathcal O(n \sqrt n + n \sqrt m)\) ,但空间复杂度降为 \(\mathcal O(m)\)

剩下三种指针移动类似,下面仅供参考

l = 1, r = 0;
for : Queries
{
    if (l > ql) v[r].push_back({ql, l - 1, i, 1});
    if (r < qr) v[l - 1].push_back({r + 1, qr, i, -1});
    if (l < ql) v[r].push_back({l, ql - 1, i, -1});
    if (r > qr) v[l - 1].push_back({qr + 1, r, i, 1});
}

[Ynoi2019 模拟赛] Yuno loves sqrt technology II

高维莫队(\(\texttt{Higher Dimensional Mo's Algorithm}\)

当询问区间变成了询问子矩阵,我们当然还是可以用莫队

如果我们将普通莫队理解为一维莫队的话,那么对于 \(k\) 维莫队

块长 \(\frac {\sqrt[k]{\prod_i n_i}} {\sqrt[2k]{m}}\) (这个块长真的很快)

其中 \(n_i\) 表示第 \(i\) 维的大小,\(m\) 表示询问个数

\(\sqrt[k]{\prod_i n_i}\) 为维度大小的几何平均

而对于排序,我们考虑按前 \(k - 1\) 维所在块分别为 第 \(1 \sim k - 1\) 关键字,第 \(k\) 维坐标为第 \(k\) 关键字

struct query
{
    int l[K], r[K], lt[K], rt[K];
    // 该询问的第 i 维的范围是 [l[i], r[i]]
    // l[i], r[i] 所在的块分别是 lt[i], rt[i]
};

inline bool operator < (const query x, const query y)
{
    for (i : 1 -> k)
        if (x.lt[i] != y.lt[i]) return x.lt[i] < y.lt[i];
    	else if (x.rt[i] != y.rt[i]) return x.rt[i] < y.rt[i];
    if (x.lt[k] != y.lt[[k]]) return x.lt[k] < y.lt[k];
   	return x.r[k] < y.r[k];
}

当然,维数较小时,可以考虑循环展开以优化常数

指针移动同理,先扩展再收缩,绝对没错

for (i : 1 -> m) // 枚举询问
	for (j : 1 -> k) // 枚举维度
    	while (l[j] > q[i].l[j]) add[j](--l[j]);
		while (r[j] < q[i].r[j]) add[j](++r[j]);
		while (l[j] < q[i].l[j]) del[j](l[j]++);
		while (r[j] > q[i].r[j]) del[j](r[j]--);

如果你还不知道函数还可以开数组,请自行百度 std::function 学习高级语法

同样地,当维数较小时,可将内层 \(j\) 的循环展开

时间复杂度 \(\mathcal O(nm^{\frac {2k - 1} {2k}})\)

其实带修改的莫队可以看作是 $\frac 3 2 $ 维莫队(

「BZOJ2639」矩形计算

More

有了高维莫队,也就有了什么高维带修改的莫队、高维树上莫队、高维树上带修改的莫队。。。

如果还有什么补充快来D我 /kk

posted @ 2022-06-29 20:50  After-glow  阅读(588)  评论(3编辑  收藏  举报