Codeforces 617E ( 莫队算法 Mo's algorithm )

原文链接:http://blog.anudeep2011.com/mos-algorithm/

本文是参考了上文的链接并做了很大改动。这个算法在Codeforces Div.1的C或D中比较常见。

本文将从按以下几个步骤来了解Mo’s algorithm:

  1. 引出一个问题
  2. 用一个简单的方法以O(n^2) 来解决这个问题
  3. 轻微修改上面的算法,它仍然运行在O(N ^ 2)
  4. 解释该算法来解决以上问题
  5. CF 617E AC_code

引出一个问题

给定一个长度为n数组,然后给出多个询问。每次询问给出一个区间[L,R] 。你需要回答这个区间内有多少个数至少重复了3次。

例如: 某个数组为{1、2、3、1、1、2、1、2、3、1 }(下标从0开始)

Query: L = 0,R = 4。 Ans= 1。 该范围为 { 1、2、3、1、1 }只有1至少重复3次。
Query: L = 1,R = 8。 Ans= 2。 该范围为 { 2、3、1、1、2、1、2、3 } 1重复3次 和 2重复3次。 重复的元素数量至少3次= = 2。


用一个简单的方法以O(n^2) 来解决这个问题

对于每个查询都从L到R进行循环,用count[]进行计数。对于每次询问都遍历一次,在最坏的情况下,即有n个询问,每个询问的区间都是[0,n-1],那么即为O(n^2)..

for each query:
  answer = 0
  count[] = 0
  for i in {l..r}:
    count[array[i]]++
    if count[array[i]] == 3:
      answer++

轻微修改上面的算法,它仍然运行在O(N ^ 2)

add(position):
  count[array[position]]++
  if count[array[position]] == 3:
    answer++

remove(position):
  count[array[position]]--
  if count[array[position]] == 2:
    answer--

currentL = 0
currentR = 0
answer = 0
count[] = 0
for each query:
  // currentL should go to L, currentR should go to R
  while currentL < L:
    remove(currentL)
    currentL++
  while currentL > L:
    add(currentL)
    currentL--
  while currentR < R:
    add(currentR)
    currentR++
  while currentR > R:
    remove(currentR)
    currentR--
  output answer

一开始是对每次询问都从L到R跑一个循环,但现在这个算法是通过改变位置将之前的询问的区间调整成为现在询问的区间。

如果之前的查询是L = 3,R = 10,那么我们将currentL = 3和currentR = 10作为上次查询的结束标志。 如果下一个查询是L = 5,R = 7,然后我们将其更新成为currentL = 5和currentR = 7。
add function表示将此位置的元素添加到当前这个区间使其更新成为新的区间,并更新相应的答案。
remove function表示将此位置的元素从当前这个区间删除使其更新成为新的区间,并更新相应的答案。


解释该算法来解决以上问题

MO’s algorithm只是将要查询的区间按照一个顺序进行询问。当给出m个询问时,我们要先把这些区间按照特殊的方法对其进行排序然后依次处理每个询问。 显然,这是一个离线算法。 每个查询都有L和R,我们将称之为左区间右区间。 一开始我们把给定的数组分成Sqrt(N)块。 每一块将为N /sqrt(N) =sqrt(N)的大小。每个左区间和右区间都需要对应这些块中的其中某一块。

例如每块的大小size,进行如下排序:

bool comp(const query a,const query b)
{
    if(a.l/size == b.l/size) return a.r < b.r;
    else return a.l/size < b.l/size;
}

这样分块排序后减少了其更新区间的复杂度。然后依次遍历排序好的m个询问。
其时间复杂度是O( N * Sqrt(N)),这里不给予证明,有兴趣的可参考原文。

正如前面提到的,这是个离线算法,这就意味着当每个询问区间内部发生变动更新的时候我们不能使用它。不仅如此对于每个add function 和 remove function,我们要根据实际情况来coding。有些情况下我们可以使用一些数据结构比如线段树来处理,但是有时候用Mo’s algorithm也是必须的。


实题演练

http://codeforces.com/problemset/problem/617/E

题意是求给定的区间内有多少种情况是符合 ai^ai+1^…^aj = k.

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
typedef long long ll;
const int N  = 1e5 + 100;
int size,a[N],cnt[1<<20];
ll res[N];
struct query{
    int l,r,id;
    friend bool operator < (const query a,const query b)
    {
        if(a.l/size == b.l/size) return a.r < b.r;
        else return a.l/size < b.l/size;
    }
}q[N];

void work(int m,int k)
{
     int curR = 0 ,curL = 1;  cnt[a[0]]++;
     ll now = 0;
     for(int i=1;i<=m;i++)        
     {
         while(curL < q[i].l)
         {
             cnt[a[curL-1]]--; now -= cnt[a[curL-1]^k]; 
             curL++;
         }
         while(curL > q[i].l)
         {
             curL--;
             now += cnt[a[curL-1]^k]; cnt[a[curL-1]]++; 
         }
         while(curR < q[i].r)
         {
             curR ++;
             now += cnt[a[curR]^k]; cnt[a[curR]]++;
         }
         while(curR > q[i].r)
         {
             cnt[a[curR]]--; now -= cnt[a[curR]^k];
             curR --;
         }
         res[q[i].id] = now;
     }

}
int main()
{
    int n,m,k; scanf("%d%d%d",&n,&m,&k);   
    size = (int)sqrt(n);

    for(int i=1;i<=n;i++) scanf("%d",&a[i]) , a[i] ^= a[i-1];
    for(int i=1;i<=m;i++) scanf("%d%d",&q[i].l,&q[i].r), q[i].id = i;

    sort(q+1,q+1+m);
    work(m,k);

    for(int i=1;i<=m;i++) printf("%I64d\n",res[i]);
    return 0;
}

你可能感兴趣的:(*Data,Structure)