莫队算法是一个非常好的算法。最简单的莫队算法用于解决一类序列上无修改只查询的区间问题。经过不同改进后,还可以解决树上路径查询问题,带修改的区间查询问题……总之,莫队算法可以解决一切区间问题。当然,莫队算法还有一个显著特征——莫队算法是一个离线算法。
考虑SPOJ3267,给定一个数组,在数组上进行
q
次询问。每次问区间
显然暴力法非常容易写,但复杂度绝对不理想。我们现在换一种形式的暴力法,令curLeft和curRight是2个指针,指向数组中的某个区间。然后根据询问,慢慢移动这2个指针到指定位置,每次移动都更新一下中间答案。当指针移动到位后,显然就得到了最后答案。为此,我们还需要一些辅助的数组。但无论如何,这都是很容易实现的。
struct _t{
int s,e;
int idx;
}B[SIZE];//记录一次询问,s、e表示该次询问的区间
int A[SIZE];//源数组
int Cnt[SIZE];//Cnt[i]表示i出现的次数,初始全为零
int MoAns;//用一个全局变量记录答案
inline void insert(int n){
++Cnt[n];//n的数量加1
if ( 1 == Cnt[n] ) ++MoAns;//说明出现了一个新的数
}
inline void remove(int n){
--Cnt[n];//n的数量减1
if ( 0 == Cnt[n] ) --MoAns;//说明有一个数彻底消失了
}
void proc(){
int curLeft = 1;
int curRight = 0;
MoAns = 0;
for(int i=0;i<Q;++i){
while( curRight < B[i].e ) insert(A[++curRight]);
while( curLeft > B[i].s ) insert(A[--curLeft]);
while( curRight > B[i].e ) remove(A[curRight--]);
while( curLeft < B[i].s ) remove(A[curLeft++]);
Ans[i] = MoAns;//第i次询问的答案
}
}
这个写法比基本暴力法复杂的多,但理论效率一样。因为为了完成一次询问,其指针的移动量也是 O(n) 的,这和基本暴力法没有区别。
但是合理安排查询的顺序以后,我们可以将上述算法流程的性能提升到一个令人满意的程度。对数组进行分块,分块的大小当然原则上是 n√ ,这样整个数组分成了 n√ 块。将所有查询进行排序。排序的第一关键字为左边界的分块索引,第二关键字为右边界。排序完成以后,再按照上述流程进行操作,依次回答每个询问。这样下来以后,可以证明完成所有查询所需的指针移动次数为 O(nn√) 。
完整的代码如下:
#include <cstdio>
#include <algorithm>
using namespace std;
int const SIZE = 30100;
int const BLOCK_SIZE = 200;//分块大小为接近根号n的整数,这样容易调试
struct _t{
int s,e;
int idx;
};
bool operator < (_t const&lhs,_t const&rhs){
int ln = lhs.s / BLOCK_SIZE;
int rn = rhs.s / BLOCK_SIZE;
return ln < rn || ( ln == rn && lhs.e < rhs.e );
}
int N,Q;
int A[SIZE];
_t B[200010];
int Ans[200010];
int Cnt[1000010] = {0};
void read(){
scanf("%d",&N);
for(int i=1;i<=N;++i)scanf("%d",A+i);
scanf("%d",&Q);
for(int i=0;i<Q;++i){
scanf("%d%d",&B[i].s,&B[i].e);
B[i].idx = i;
}
}
int MoAns;
inline void insert(int n){
++Cnt[n];
if ( 1 == Cnt[n] ) ++MoAns;
}
inline void remove(int n){
--Cnt[n];
if ( 0 == Cnt[n] ) --MoAns;
}
void Mo(){
sort(B,B+Q);
int curLeft = 1;
int curRight = 0;
MoAns = 0;
for(int i=0;i<Q;++i){
while( curRight < B[i].e ) insert(A[++curRight]);
while( curLeft > B[i].s ) insert(A[--curLeft]);
while( curRight > B[i].e ) remove(A[curRight--]);
while( curLeft < B[i].s ) remove(A[curLeft++]);
Ans[B[i].idx] = MoAns;
}
}
int main(){
//freopen("1.txt","r",stdin);
read();
Mo();
for(int i=0;i<Q;++i)printf("%d\n",Ans[i]);
return 0;
}
莫队算法使用了分块的思想,但实际上在基本算法中,每一个块并不需要维持什么特征值,分块仅用来为查询排序。另一方面,很明显,这样的移动并不能保证是移动次数最少的。所以,理论上,莫队算法将查询看做是二维平面上的点,定义点的距离为Manhattan距离,然后求出这若干个点所构成的Manhattan最小生成树,按照最小生成树即可移动最少次数以完成所有查询。但是毫无疑问,这样的写法太复杂。所以按照分块思想排序后再查询,兼顾了时间复杂度与代码复杂度,可以说的上是一个非常优秀的算法。