莫队算法,用来处理一类 离线 区间查询问题,具有编程复杂度低,易于扩展的优点。
可以方便地扩展到树上,也可以以一定的代价支持修改。
序列上 + 无修改
BZOJ 2038 小 Z 的袜子
这是最普通的莫队问题。
如果序列上询问区间 [l,r] 上的答案可以 O(1) 地扩展到 [l−1,r],[l+1,r],[l,r−1],[l,r+1] 上,也就是说,我们能够 O(1) 的添加和删除一个位置的贡献,那么莫队可以在 O(n√n) 的时间内处理所有询问。
我们先来考虑基于区间转移的暴力:按顺序处理询问,每次暴力(一直移动区间端点,添加或删除一个位置的贡献,直到与当前区间相同)从上一个区间转移到下一个即可。
我们大概可以写出以下代码:
struct Query{
int l, r;
int id;
} querys[MAXM]; // 询问区间 [l, r],id 代表在输入顺序中该询问的编号
int ans[MAXM]; // 每个询问的答案
bool in[MAXN]; // 每个元素的贡献是否在答案中
int l, r, currAns; // 维护当前区间 [l, r] 的答案是 currAns
void flip(int x){ // 将 x 的存在性取反
in[x] ^= 1;
if(in[x]){
// add the contribution of x into currAns;
} else{
// del the contribution of x from currAns;
}
}
void solve(){
l = 0, r = 0, currAns = 0, flip(0);
for(int i = 0; i < m; i++){
Query *q = &querys[i];
while(l > q->l) flip(--l);
while(r < q->r) flip(++r);
while(l < q->l) flip(l++);
while(r > q->r) flip(r--);
ans[q->id] = currAns;
}
}
每次转移的复杂度是 O(n) 的,有 O(m) 次转移,那么复杂度是 O(nm) 的,我们以 m,n 同阶(以下同),则复杂度是 O(n2) 的。
这个算法看起来并不比直接暴力优越,但我们考虑其意义,将区间 [l,r] 看做二维平面上的点 (l,r),可以看出,区间之间的转移代价是他们的「曼哈顿距离」。
(曼哈顿距离:平面上两个点 (a,b) 和 (c,d) 的曼哈顿距离为 |a−c|+|b−d|,即横纵坐标差的绝对值之和)
那么处理所有的询问相当于按照某种顺序走过所有点,所用总时间即为走过的曼哈顿距离之和。
一种可行的思路是,求出所有点的「最小曼哈顿距离生成树」,然后在生成树上走。
然而一点都不好写辣...
接下来我们说「莫队」的做法。
设置一个块的大小 S,根据左端点将询问分成若干块,即左端点为 l 的询问被分到块 ⌊lS⌋ 中,然后在每一块中根据 r 排序,然后块内暴力转移,块间也暴力转移。
另一种更简明的描述方法:按将询问按 (⌊lS⌋,r) 二元组排序,然后按这个顺序从前往后暴力转移。
可是等等,这可不就是把询问序列调了个顺序然后暴力嘛,怎么复杂度就变成 O(n√n) 了呢?
然而确实是 O(n√n),下面讨论时间复杂度。
我们先考虑块内的转移,对于每个询问,块内左端点移动是 O(S) 的,对于每个块,总体来计,右端点的移动是 O(n) 的,块数是 O(nS) 的,所以块内转移的总复杂度是 O(nS+n2S) 的。
然后考虑块间的转移,每次转移是 O(n) 的,块数是 O(nS) 的,所以块间转移的总复杂度是 O(n2S) 的。
把两种转移加起来,算法的总复杂度是 O(nS+n2S)=O(n(S+nS)),根据基本不等式,当 S 取 √n 时最优,这时的复杂度为 O(n√n)。
至于实现上,就是先计算 S,然后把询问按以上规则排序,之后暴力转移即可,代码与上面给出的完全一致。
可以在这篇文章中 [BZOJ 2038] 小 Z 的袜子 - 莫队 看到本题的完整代码。
序列上 + 带修改
BZOJ 2120 数颜色
如果带修改怎么办?
如果我们能够 O(1) 地添加或删除一个修改的贡献,那么莫队算法可以在 O(n53) 的时间内处理所有询问。
我们可以把考虑修改操作看成是添加一维,这一维是时间。
即把询问表示成 (l,r,t) 的形式,其中 t 表示在这之间有多少个询问。
可以把这看成三维空间上的点,然后暴力移动三个坐标以转移。
我们大概可以写出这样的代码:
struct Query{
int l, r, time;
int id;
} querys[MAXM]; // 询问区间 [l, r],time 代表询问时间,或者说是在这之前的修改次数,id 代表在输入顺序中该询问的编号
int ans[MAXM]; // 每个询问的答案
bool in[MAXN]; // 每个元素的贡献是否在答案中
int l, r, currTime, currAns; // 维护当前询问 (l, r, currTime) 的答案是 currAns
void flip(int x){ // 将 x 的存在性取反
in[x] ^= 1;
if(in[x]){
// add the contribution of x into currAns;
} else{
// del the contribution of x from currAns;
}
}
void flipModification(int x){
// apply modification or invoke modification;
// if necessary, update currAns;
}
void solve(){
l = 0, r = 0, currAns = 0, currTime = 0, flip(0);
for(int i = 0; i < m; i++){
Query *q = &querys[i];
while(currTime < q->time) flipModification(currTime++);
while(currTime > q->time) flipModification(--currTime);
while(l > q->l) flip(--l);
while(r < q->r) flip(++r);
while(l < q->l) flip(l++);
while(r > q->r) flip(r--);
ans[q->id] = currAns;
}
}
保证时间复杂度的方法是,设定块的大小 S1,S2,将询问按 (⌊lS1⌋,⌊rS2⌋,t) 三元组排序。
讨论下时间复杂度,以修改数、询问数均与 n 同阶。
考虑这样的小块,其中的询问第一第二关键字相同,那么在块内,t 的移动是 O(n) 的,一共有 O(n2S1S2) 个块,所以转移 t 的复杂度是 O(n3S1S2)。
对于每个询问,l 的移动是 O(S1) 的,r 的移动是 O(S2) 的。
总的复杂度 O(n3S1S2+nS1+nS2)=O(n(n2S1S2+S1+S2)),根据(某种我不知道的数学技巧),当 S1=S2=n23 时最优,此时复杂度为 O(n53)
代码实现上,仍然是先计算 S,然后把询问按以上规则排序,然后像如上代码一样暴力转移即可。
可以在这篇文章中 [BZOJ 2120] 数颜色 - 带修改莫队 看到本题的完整代码。
树上 + 无修改
SPOJ COT2 Count on a tree II
接下来我们把莫队弄到树上去。~~(当然不是加个树链剖分)~~
原来的区间询问变成了树上路径询问。
如果我们能 O(1) 的添加或删除一个点的贡献,那么莫队可以在 O(n√n) 的时间内处理所有询问。
我们仍然先考虑暴力转移,这次我们转移的对象变成了路径,怎么办呢。
我们设 Ri 表示从 i 到根路径上的点集(包括 i),(u,v) 表示从 u 到 v 路径上的点集,那么有:
(u,v)=Ru⊗Rv⊗{lca(u,v)}
(其中 ⊗ 表示对称差运算,通俗地说就是出现两次的元素会被消掉)
我们要转移到 u′,v′,也就是:
(u′,v′)=Ru′⊗Rv′⊗{lca(u′,v′)}
根据对称差的性质,空集是单位元,任何集合都是其自身的逆元,以及交换律和结合律,有:
(u′,v′)=(u,v)⊗({lca(u,v)}⊗{lca(u′,v′)})⊗(Ru⊗Ru′)⊗(Rv⊗Rv′)=(u,v)⊗(lca(u,v)⊗{lca(u′,v′)})⊗((u,u′)⊗{lca(u,u′)})⊗((v,v′)⊗{lca(v,v′)})
在程序中实现对称差运算,就是对存在性取反,类似上面的 flip
函数。
那么实现这个转移的完整步骤就是这样的:
一、先对原来的 lca(u,v) 取反,再对新的 lca(u′,v′) 取反,其实这样就把原来的 lca 替换成现在的了。
二、然后对路径 (u,u′) 上除 lca(u,u′) 以外的点取反,对路径 (v,v′) 上除 lca(v,v′) 以外的点取反,直接暴力遍历路径上的点即可。
我们大概可以写出如下代码(Node
类型是树上节点):
struct Query{
Node *u, *v;
int time;
int id;
} querys[MAXM];
int currAns;
inline void flip(Node *v){ // 对 v 的存在性取反
v->in ^= 1;
if(v->in){
// add the contribution of v into currAns;
} else{
// del the contribution of v from currAns;
}
}
inline void flip(Node *u, Node *v){ // 对 (u, v) 路径上除 lca(u, v) 以外的点的存在性取反
Node *lca = queryLCA(u, v);
for(Node *p = u; p != lca; p = p->fa) flip(p);
for(Node *p = v; p != lca; p = p->fa) flip(p);
}
inline void solve(){
Node *u = nodes, *v = nodes, *lca = nodes;
currAns = 0, flip(nodes);
for(int i = 0; i < m; i++){
Query *q = &querys[i];
// 步骤一
flip(lca);
lca = queryLCA(q->u, q->v);
flip(lca);
// 步骤二
flip(u, q->u), u = q->u;
flip(v, q->v), v = q->v;
ans[q->id] = currAns;
}
}
下面讨论如何排序以保证时间复杂度。
首先你要会对树分块,所以请参见这道题目 [BZOJ 1086] 王室联邦 - 树分块。
这种分块的特点是块内任意两点间距离是 O(S) 的。
排序方法是:将询问按照 (blockId[u],dfn[v]) 二元组排序。
和普通莫队的分析类似的,我们可以得到当 S=√n 时,复杂度为 O(n√n)
可以在这篇文章中 Count on a tree II - 树上莫队 看到本题的完整代码。
树上 + 带修改
BZOJ 3052 糖果公园(在 UOJ 58 上也有此题)
~~(卡评测好题)~~
相信您看到这里,应该已经掌握了莫队的套路啦,对于这种带修改的情形,我们加一维时间就是咯。
排序依据是三元组 (blockId[u],blockId[v],t),时间复杂度为 O(n53)
我还是把转移的大致代码给一下吧,作为参考:
struct Query{
Node *u, *v;
int id;
} querys[MAXM];
int currAns, currTime;
inline void flip(Node *v){ // 对 v 的存在性取反
v->in ^= 1;
if(v->in){
// add the contribution of v into currAns;
} else{
// del the contribution of v from currAns;
}
}
inline void flip(Node *u, Node *v){ // 对 (u, v) 路径上除 lca(u, v) 以外的点的存在性取反
Node *lca = queryLCA(u, v);
for(Node *p = u; p != lca; p = p->fa) flip(p);
for(Node *p = v; p != lca; p = p->fa) flip(p);
}
void flipModification(int x){
// apply modification or invoke modification;
// if necessary, update currAns;
}
inline void solve(){
Node *u = nodes, *v = nodes, *lca = nodes;
currAns = 0, currTime = 0, flip(nodes);
for(int i = 0; i < m; i++){
Query *q = &querys[i];
while(currTime < q->time) flipModification(currTime++);
while(currTime > q->time) flipModification(--currTime);
// 步骤一
flip(lca);
lca = queryLCA(q->u, q->v);
flip(lca);
// 步骤二
flip(u, q->u), u = q->u;
flip(v, q->v), v = q->v;
ans[q->id] = currAns;
}
}
可以在这篇文章中 [BZOJ 3052] 糖果公园 - 树上带修改莫队 看到本题的完整代码。
总结
总的来说,莫队算法还是比较好懂的,也十分好写,因为其本质是优化的暴力。
对莫队算法的扩展思路可能会给我们一些启示。
- 加一维支持修改
- 将序列问题扩展到树上 ~~或许也可以扩展到仙人掌上(雾~~
参考资料
莫队算法学习笔记, Sengxian
清橙A1206 小Z的袜子(莫队算法), 疯狂的橡树
WC 2013 糖果公园 park 题解, vfleaking