假设
离线后排序,顺序处理每个询问,暴力从上一个区间的答案转移到下一个区间答案(一步一步移动即可)。
对于区间
inline 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;
}
}
以下的情况在
首先是分块这一步,这一步的时间复杂度是
接着就到了莫队算法的精髓了,下面我们用通俗易懂的初中方法来证明它的时间复杂度是
证:令每一块中
由第一次排序可知,$\max_1 \le \max_2 \le \cdots \le \max_{\lceil\sqrt{n}\rceil}$。
显然,对于每一块暴力求出第一个询问的时间复杂度为
考虑最坏的情况,在每一块中,$R$ 的最大值均为
考虑
重点分析
将每一块
对于
$$ \begin{aligned} & O(\sqrt{n}(\max{}_1-1)+\sqrt{n}(\max{}_2-\max{}_1)+\sqrt{n}(\max{}3-\max{}2)+\cdots+\sqrt{n}(\max{}{\lceil\sqrt{n}\rceil}-\max{}{\lceil\sqrt{n}\rceil-1))} \ = & O(\sqrt{n}\cdot(\max{}1-1+\max{}2-\max{}1+\max{}3-\max{}2+\cdots+\max{}{\lceil\sqrt{n}\rceil-1}-\max{}{\lceil\sqrt{n}\rceil-2}+\max{}{\lceil\sqrt{n}\rceil}-\max{}{\lceil\sqrt{n}\rceil-1)}) \ = & O(\sqrt{n}\cdot(\max{}{\lceil\sqrt{n}\rceil-1}))\ \end{aligned} $$
(裂项求和)
由题可知
综上所述,莫队算法的时间复杂度为
但是对于
怎么分块呢?
我们设块长度为
事实上,如果块长度的设定不准确,则莫队的时间复杂度会受到很大影响。例如,如果
莫队算法看起来十分暴力,很大程度上是因为莫队算法的分块排序方法看起来很粗糙。我们会想到通过看上去更精细的排序方法对所有区间排序。一种方法是把所有区间
假设
莫队算法还有一个特点:当
[!NOTE] 例题「国家集训队」小 Z 的袜子
题目大意:
有一个长度为
$n$ 的序列${c_i}$ 。现在给出$m$ 个询问,每次给出两个数$l,r$ ,从编号在$l$ 到$r$ 之间的数中随机选出两个不同的数,求两个数相等的概率。
思路:莫队算法模板题。
对于区间
然后从序列的第一个询问开始计算答案,第一个询问通过直接暴力算出,复杂度为
具体做法:
对于区间
我们设
而这个询问的答案就是
这里有个优化:$\displaystyle C_a^2=\frac{a (a-1)}{2}$。
所以
所以
算法总复杂度:$O(n\sqrt{n} )$
下面的代码中 deno
表示答案的分母 (denominator),nume
表示分子 (numerator),sqn
表示块的大小:$\sqrt{n}$,arr
是输入的数组,node
是存储询问的结构体,tab
是询问序列(排序后的),col
同上所述。
注意:由于 ++l
和 --r
的存在,下面代码中的移动区间的 4 个 while 循环的位置很关键,不能随意改变它们之间的位置关系。
[!NOTE] 关于四个循环位置的讨论
莫队区间的移动过程,就相当于加入了
$[1,r]$ 的元素,并删除了$[1,l-1]$ 的元素。因此,
对于
$l\le r$ 的情况,$[1,l-1]$ 的元素相当于被加入了一次又被删除了一次,$[l,r]$ 的元素被加入一次,$[r+1,+\infty)$ 的元素没有被加入。这个区间是合法区间。对于
$l=r+1$ 的情况,$[1,r]$ 的元素相当于被加入了一次又被删除了一次,$[r+1,+\infty)$ 的元素没有被加入。这时这个区间表示空区间。对于
$l>r+1$ 的情况,那么$[r+1,l-1]$ (这个区间非空)的元素被删除了一次但没有被加入,因此这个元素被加入的次数是负数。因此,如果某时刻出现
$l>r+1$ 的情况,那么会存在一个元素,它的加入次数是负数。这在某些题目会出现问题,例如我们如果用一个set
维护区间中的所有数,就会出现“需要删除set
中不存在的元素”的问题。代码中的四个 while 循环一共有
$4!=24$ 种排列顺序。不妨设第一个循环用于操作左端点,就有以下$12$ 种排列(另外$12$ 种是对称的)。下表列出了这 12 种写法的正确性,还给出了错误写法的反例。
循环顺序 正确性 反例或注释 l--,l++,r--,r++
错误 $l<r<l'<r'$ l--,l++,r++,r--
错误 $l<r<l'<r'$ l--,r--,l++,r++
错误 $l<r<l'<r'$ l--,r--,r++,l++
正确 证明较繁琐 l--,r++,l++,r--
正确 l--,r++,r--,l++
正确 l++,l--,r--,r++
错误 $l<r<l'<r'$ l++,l--,r++,r--
错误 $l<r<l'<r'$ l++,r++,l--,r--
错误 $l<r<l'<r'$ l++,r++,r--,l--
错误 $l<r<l'<r'$ l++,r--,l--,r++
错误 $l<r<l'<r'$ l++,r--,r++,l--
错误 $l<r<l'<r'$ 全部 24 种排列中只有 6 种是正确的,其中有 2 种的证明较繁琐,这里只给出其中 4 种的证明。
这 4 种正确写法的共同特点是,前两步先扩大区间(
l--
或r++
),后两步再缩小区间(l++
或r--
)。这样写,前两步是扩大区间,可以保持$l\le r+1$ ;执行完前两步后,$l\le l'\le r'\le r$ 一定成立,再执行后两步只会把区间缩小到$[l',r']$ ,依然有$l\le r+1$ ,因此这样写是正确的。
[!TIP] 参考代码
我们看一下下面这组数据
// 设块的大小为 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% 左右。
排序代码:
压行
// 这里有个小细节等下会讲
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;
}
};
不压行
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;
}
};
Warning
小细节:如果使用 sort 比较两个函数,不能出现
对于压行版,如果没有 r == x.r
的特判,当 l 属于同一奇数块且 r 相等时,会出现上面小细节中的问题(自己手动模拟一下),对于压行版,如果写成小于(大于)等于,则也会出现同样的问题。