莫队算法详解

莫队算法详解

今天模拟赛T2需要用到莫队,这篇博客就聊聊莫队。

算法简介:

莫队算法是由莫涛提出的算法。在莫涛提出莫队算法之前,莫队算法已经在 Codeforces 的高手圈里小范围流传,但是莫涛是第一个对莫队算法进行详细归纳总结的人。莫涛提出莫队算法时,只分析了普通莫队算法,但是经过 OIer 和 ACMer 的集体智慧改造,莫队有了多种扩展版本。

莫队算法可以解决一类离线区间询问问题,适用性极为广泛。同时将其加以扩展,便能轻松处理树上路径询问以及支持修改操作。


以上内容来自oiwiki

好了言归正传,我们先用一道耳熟能详的经典例题来引入:
Description

有n个数字,给出k,以及m个查询。
每次查询的格式是L,r,求L~r(左右包含)这个区间内数字的出现次数刚好是k的数字种数。
范围:n<=30000,k<=n,m<=30000,1<=L<r<=n,数列中元素大小<=n。
输入n,k,m,然后n个元素,然后m行查询,对于每一个询问,输出正确的答案。

Example input:
5 2 3
1 2 3 2 2
1 2
2 4
1 5

Example output:

0
1
0
我们先来考虑一下每个人都会的暴力方法——挨个区间枚举。肯定对,但是时间复杂度O(N*M)显然不够优秀。

于是莫队横空出世!

我们把询问区间的L和R的大小进行分块。
比如n=9,有以下的询问:
2 3
1 4
4 5
1 6
7 9
8 9
5 8
6 8
对于n=9,我们把这些询问按左端点大小以每√n位一块分块,即1-3,4-6,7-9分别为一块。对于每个询问,我们按左端点从小到大分在一个块里。
那么上面的询问就可以分组成:
(2,3),(1,4),(1,6)
(4,5),(5,8),(6,8)
(7,9),(8,9)
这步操作我们通过sort和cmp来实现

bool cmp(Query x,Query y)
{
    if ((x/block)!=(y/block))
        return x.L<y.L;        //不同块的时候
    return x.r<y.r;      //同一块的时候
}

之后对于一个查询,我们将Left指针逐步更新成新的L,Right同理。比如一开始Left=2,Right=3,而L=1,r=5。那么我们Left-1,并且把Left位置上的数字出现次数+1。Right+1,把Right位置上的数字出现次数+1,直到Right=5为止。
代码实现:

add(x)
{  //把x位置的数字加入进来
    cnt[x]++;
    if(cnt[x]==k) ans++;
}
remove(x)
{  //把x位置的数字移出去
    cnt[x]--;
    if(cnt[x]==k-1) ans--;
}
Left=Right=1;
add(1);
ans=0;
for u=1 to m
{
    while (Left<L[u]){remove(Left);Left++;}
    while (Left>L[u]){ Left--;add(Left);}
    while (Right<r[u]){Right++;add(Right};}
    while (Right>r[u]){remove(Right);Right--;}
    output ans;
}

这样,莫队算法就写完啦!
复杂度分析:
那么我们一通操作猛如虎,最后到底降低了多少复杂度呢?
在同一个块的时候。由于L的范围是确定的,所以每次L的偏移量是O(√N),但是r的范围没有确定;r的偏移量是O(N)。那么从一个块到另一个块呢?明显地,r我们不需要作考虑,仍然是O(N)。而L明显最多也是2√N,而且这种情况下,很快就会到下下一块。所以也是O(√N),由于有√N(根号N)个块,所以r的总偏移量是O(N√N),而M个询问,每个询问都可以让L偏移O(√N),所以L的总偏移量O(M*√N),注意了,时间复杂度分析的时候一定要注意,r的偏移量和询问数目是没有直接关系的。而L则恰恰相反;L的偏移量我们刚才也说明了,它和块的个数没有直接关系。所以总的时间复杂度是:

O((N+M)*√N)

这就比一开始的无脑暴力快多了!但是这样就表明,莫队是一个离线算法,如果题目强制在线,莫队就无能为力了。
最后放上完整莫队框架:

add(x)
{  
    cnt[x]++;
    //根据题意填
}
remove(x)
{ 
    cnt[x]--;
    //根据题意填
}
Left=Right=1;
add(1);
ans=0;
for u=1 to m
{
    while (Left<L[u]){remove(Left);Left++;}
    while (Left>L[u]){ Left--;add(Left);}
    while (Right<r[u]){Right++;add(Right};}
    while (Right>r[u]){remove(Right);Right--;}
    output ans;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值