莫队算法详解

[点击打开原博客](https://blog.csdn.net/thinfatty/article/details/72581276%20%E7%82%B9%E5%87%BB%E6%89%93%E5%BC%80%E5%8E%9F%E5%8D%9A%E5%AE%A2)

莫队算法我早有耳闻。。可惜前不久才去学习。

但是自己看了看论文,也就1h左右,就能够全部理解了。

也就是说其实这个算法不难。。

好了,让我们进入正题。


我们首先来看一道例题:

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     //2

0     //没有(2太多了,也不算)

我们来考虑一下这题的解法。

首先肯定可以万能暴力,每次L~r枚举。时间复杂度O(N*M)。

但是这样的暴力是没有前途的!我们来考虑一下新的暴力:

一开始指针区间0->0,然后对于一个查询,我们将Left指针逐步更新成新的L,Right同理。

比如一开始Left=2,Right=3,而L=1,r=5。

那么我们Left-1,并且把Left位置上的数字出现次数+1.

Right+1,把Right位置上的数字出现次数+1,直到Right=5为止。

框架:


    
    
  1. add(x){  //把x位置的数字加入进来
  2.     cnt[x]++;
  3.     if (cnt[x]==k) ans++;
  4. }
  5. remove(x){  //把x位置的数字移出去
  6.     cnt[x]–;
  7.     if (cnt[x]==k -1) ans–;
  8. }

然后以上面题目为例;这种方法需要离线处理,我们同理来看一下解法:


    
    
  1. Left=Right= 1; add( 1);
  2. ans= 0;
  3. for u= 1 to m{
  4.     while (Left<L[u]){  remove(Left); Left++;}
  5.     while (Left>L[u]){  Left--; add(Left);}
  6.     while (Right<r[u]){  Right++; add(Right};}
  7.     while (Right>r[u]){  remove(Right); Right--;}
  8.     output ans;
  9. }

这里说明一下,其实remove(Left);Left–;等等可以直接写成remove(Left–)等等,我这么写是为了理解。

分析一下时间复杂度,我们可以从Left和Right的移动量来分析:

每一个新的询问,Left和Right的移动量最大都会是O(N)

所以这样子的方法时间复杂度仍然是O(N*M),而且可能比上面的暴力更慢。

但是莫队算法的核心,就是从这么一个算法转变过来的。

现在来介绍一下莫队算法解决这道题:

对询问进行分块,我们知道m个询问,L和r的范围都在n以内,我们根据L和r的大小来对询问分块。

比如n=9,有以下的询问:

2 3

1 4

4 5

1 6

7 9

8 9

5 8

6 8

对于n=9,我们以根号n为每个块block的大小,这里block=3.

那么我们把1~3分成一组,4~6,7~9.

对于每一个询问(L,r),我们以L的范围来决定这个询问在哪一个块。

然后每一个独自的块内,我们让询问r更小的排在更前面。

那么上面的询问就可以分组成:

(2,3)/(1,4)/(1,6)和

(4,5)/(5,8)/(6,8)和

(7,9)/(8,9)

这一步的排序操作,我们可以在排序的时候加入判断条件cmp:


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


排序之后,我们再来分析一下时间复杂度;接下来我们会看到神奇的事情!!

刚才分析此方法的时候,我们是从L和R的偏移量分析的;我们仍然用这种方法来分析。

考虑一下在同一个块的时候。由于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)

很神奇地看到了,我们仅仅改变了一下问题求解的次序,就让时间复杂度大幅度下降!

当然这个说明过程我们也看到了,事实上,莫队是一个必须离线的算法。

意味着一些题目如果强制在线,那么莫队就无能为力了。


代码就不总结了,可以看上面的分块的地方,也可以看一些莫队的题目的题解。


            </div>
[点击打开原博客](https://blog.csdn.net/thinfatty/article/details/72581276%20%E7%82%B9%E5%87%BB%E6%89%93%E5%BC%80%E5%8E%9F%E5%8D%9A%E5%AE%A2)

莫队算法我早有耳闻。。可惜前不久才去学习。

但是自己看了看论文,也就1h左右,就能够全部理解了。

也就是说其实这个算法不难。。

好了,让我们进入正题。


我们首先来看一道例题:

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     //2

0     //没有(2太多了,也不算)

我们来考虑一下这题的解法。

首先肯定可以万能暴力,每次L~r枚举。时间复杂度O(N*M)。

但是这样的暴力是没有前途的!我们来考虑一下新的暴力:

一开始指针区间0->0,然后对于一个查询,我们将Left指针逐步更新成新的L,Right同理。

比如一开始Left=2,Right=3,而L=1,r=5。

那么我们Left-1,并且把Left位置上的数字出现次数+1.

Right+1,把Right位置上的数字出现次数+1,直到Right=5为止。

框架:


  
  
  1. add(x){  //把x位置的数字加入进来
  2.     cnt[x]++;
  3.     if (cnt[x]==k) ans++;
  4. }
  5. remove(x){  //把x位置的数字移出去
  6.     cnt[x]–;
  7.     if (cnt[x]==k -1) ans–;
  8. }

然后以上面题目为例;这种方法需要离线处理,我们同理来看一下解法:


  
  
  1. Left=Right= 1; add( 1);
  2. ans= 0;
  3. for u= 1 to m{
  4.     while (Left<L[u]){  remove(Left); Left++;}
  5.     while (Left>L[u]){  Left--; add(Left);}
  6.     while (Right<r[u]){  Right++; add(Right};}
  7.     while (Right>r[u]){  remove(Right); Right--;}
  8.     output ans;
  9. }

这里说明一下,其实remove(Left);Left–;等等可以直接写成remove(Left–)等等,我这么写是为了理解。

分析一下时间复杂度,我们可以从Left和Right的移动量来分析:

每一个新的询问,Left和Right的移动量最大都会是O(N)

所以这样子的方法时间复杂度仍然是O(N*M),而且可能比上面的暴力更慢。

但是莫队算法的核心,就是从这么一个算法转变过来的。

现在来介绍一下莫队算法解决这道题:

对询问进行分块,我们知道m个询问,L和r的范围都在n以内,我们根据L和r的大小来对询问分块。

比如n=9,有以下的询问:

2 3

1 4

4 5

1 6

7 9

8 9

5 8

6 8

对于n=9,我们以根号n为每个块block的大小,这里block=3.

那么我们把1~3分成一组,4~6,7~9.

对于每一个询问(L,r),我们以L的范围来决定这个询问在哪一个块。

然后每一个独自的块内,我们让询问r更小的排在更前面。

那么上面的询问就可以分组成:

(2,3)/(1,4)/(1,6)和

(4,5)/(5,8)/(6,8)和

(7,9)/(8,9)

这一步的排序操作,我们可以在排序的时候加入判断条件cmp:


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


排序之后,我们再来分析一下时间复杂度;接下来我们会看到神奇的事情!!

刚才分析此方法的时候,我们是从L和R的偏移量分析的;我们仍然用这种方法来分析。

考虑一下在同一个块的时候。由于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)

很神奇地看到了,我们仅仅改变了一下问题求解的次序,就让时间复杂度大幅度下降!

当然这个说明过程我们也看到了,事实上,莫队是一个必须离线的算法。

意味着一些题目如果强制在线,那么莫队就无能为力了。


代码就不总结了,可以看上面的分块的地方,也可以看一些莫队的题目的题解。


            </div>

猜你喜欢

转载自blog.csdn.net/wookaikaiko/article/details/81191480
今日推荐