5171: 学习系列 —— 莫队算法(Mo's Algorithm)
[Creator : ]
Description
【算法来源】
莫队算法是由莫涛提出的算法。在莫涛提出莫队算法之前,莫队算法已经在 Codeforces 的高手圈里小范围流传,但是莫涛是第一个对莫队算法进行详细归纳总结的人。莫涛提出莫队算法时,只分析了普通莫队算法,但是经过 OIer 和 ACMer 的集体智慧改造,莫队有了多种扩展版本。
莫队算法可以解决一类离线区间询问问题,适用性极为广泛。同时将其加以扩展,便能轻松处理树上路径询问以及支持修改操作。
【普通莫队】
普通莫队不支持修改。假设 $n=m$,那么对于序列上的区间询问问题,如果从 $[l,r]$ 的答案能够 $O(1)$ 扩展到 $[l-1,r],[l+1,r],[l,r+1],[l,r-1]$(即与 $[l,r]$ 相邻的区间)的答案,那么可以在 $O(n\times \sqrt n)$ 的复杂度内求出所有询问的答案。
离线后排序,顺序处理每个询问,暴力从上一个区间的答案转移到下一个区间答案(一步一步移动即可)。
对于区间 $[l,r]$, 以 $l$ 所在块的编号为第一关键字,$r$ 为第二关键字从小到大排序。
void move(int pos, int sign) { // update nowAns } void solve() { BLOCK_SIZE = int(ceil(pow(n, 0.5))); sort(querys, querys + m); for (int i = 0; i < m; ++i) { const query &q = querys[i]; while (l > q.l) move(--l, 1); while (r < q.r) move(++r, 1); while (l < q.l) move(l++, -1); while (r > q.r) move(r--, -1); ans[q.id] = nowAns; } }注意:由于 ++l 和 --r 的存在,下面代码中的移动区间的 4 个 while 循环的位置很关键,不能随意改变它们之间的位置关系。
莫队算法看起来十分暴力,很大程度上是因为莫队算法的分块排序方法看起来很粗糙。我们会想到通过看上去更精细的排序方法对所有区间排序。一种方法是把所有区间 $[l, r]$ 看成平面上的点 $(l, r)$,并对所有点建立曼哈顿最小生成树,每次沿着曼哈顿最小生成树的边在询问之间转移答案。这样看起来可以改善莫队算法的时间复杂度,但是实际上对询问分块排序的方法的时间复杂度上界已经是最优的了。
莫队算法还有一个特点:当 $n$ 不变时,$m$ 越大,处理每次询问的平均转移代价就越小。一些其他的离线算法也具有同样的特点(如求 LCA 的 Tarjan 算法),但是莫队算法的平均转移代价随 $m$ 的变化最明显。
【普通莫队的优化】
我们看一下下面这组数据// 设块的大小为 2 (假设) 1 1 2 100 3 1 4 100手动模拟一下可以发现,r 指针的移动次数大概为 300 次,我们处理完第一个块之后,l = 2, r = 100,此时只需要移动两次 l 指针就可以得到第四个询问的答案,但是我们却将 r 指针移动到 1 来获取第三个询问的答案,再移动到 100 获取第四个询问的答案,这样多了九十几次的指针移动。我们怎么优化这个地方呢?这里我们就要用到奇偶化排序。
什么是奇偶化排序?奇偶化排序即对于属于奇数块的询问,r 按从小到大排序,对于属于偶数块的排序,r 从大到小排序,这样我们的 r 指针在处理完这个奇数块的问题后,将在返回的途中处理偶数块的问题,再向 n 移动处理下一个奇数块的问题,优化了 r 指针的移动次数,一般情况下,这种优化能让程序快 30% 左右。
【不压行代码】
struct node { int l, r, id; bool operator<(const node &x) const { if (l / unit != x.l / unit) return l < x.l; // 注意下面两行不能写小于(大于)等于,否则会出错(详见下面的小细节) if ((l / unit) & 1) return r < x.r; return r > x.r; } };
【压行代码】
// 这里有个小细节等下会讲 int unit; // 块的大小 struct node { int l, r, id; bool operator<(const node &x) const { return l / unit == x.l / unit ? (r == x.r ? 0 : ((l / unit) & 1) ^ (r < x.r)) : l < x.l; } };对于压行版,如果没有 r == x.r 的特判,当 l 属于同一奇数块且 r 相等时,会出现上面小细节中的问题(自己手动模拟一下),对于不压行版,如果写成小于(大于)等于,则也会出现同样的问题。
参考自 OI Wiki 普通莫队。