莫队算法

莫队算法是一个非常好的算法。最简单的莫队算法用于解决一类序列上无修改只查询的区间问题。经过不同改进后,还可以解决树上路径查询问题,带修改的区间查询问题……总之,莫队算法可以解决一切区间问题。当然,莫队算法还有一个显著特征——莫队算法是一个离线算法。

考虑SPOJ3267,给定一个数组,在数组上进行 q 次询问。每次问区间[i,j]中不同元素的总数有多少。

显然暴力法非常容易写,但复杂度绝对不理想。我们现在换一种形式的暴力法,令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最小生成树,按照最小生成树即可移动最少次数以完成所有查询。但是毫无疑问,这样的写法太复杂。所以按照分块思想排序后再查询,兼顾了时间复杂度与代码复杂度,可以说的上是一个非常优秀的算法。

  • 10
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值