莫队算法专题

概览

莫对算法是一个对于区间、树或其他结构离线(在线)维护的算法,此算法基于一些基本算法,例如暴力维护,树状数组,分块,最小曼哈顿距离生成树,对其进行糅合从而产生的一个简单易懂且短小好写的算法
此算法在很多情况下可以很轻松的切掉一些复杂而且难写的数据结构问题。

引例

  • 给定一个大小为N的数组,数组中所有元素的大小<=N。你需要回答M个查询。每个查询的形式是L,R。你需要回答在范围[L,R]中至少重复3次的数字的个数。
  • 例如:数组为{1,2,3,1,1,2,1,2,3,1}(索引从0开始)
  • 查询:L = 0, R = 4。答案 = 1。在范围[L,R]中的值={1,2,3,1,1},只有1是至少重复3次的。
  • 查询:L = 1,R = 8。答案 = 2。在范围[L,R]中的值={2,3,1,1,2,1,2,3},1重复3遍并且2重复3遍。至少重复3次的元素数目 = 答案 = 2。

复杂度为O(N^2)的简单解法

  • 对于每个查询,从L至R循环,统计元素出现频率,报告答案。考虑M=N的情况,以下程序在最坏的情况运行在O(n^2)
for each euqry:
	answer = 0
	count[] = 0
	for i in {l...r}:
		count[array[i]]++
		if count[array[i]] == 3
			answer++
  • 对上述算法稍作修改。它仍然运行在 O ( n 2 ) O(n^2) O(n2)

对上述算法稍作修改。它仍然运行在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应当到L,currentR应当到R
	while currentL < L:
		remove(currentL)
		currentL++
	while currentL > L:
		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函数意味着我们添加该位置的元素到当前集合内,并且更新相应的回答。
  • remove函数意味着我们从当前集合内移除该位置的元素,并且更新相应的回答

莫队算法思路

  • 莫队算法仅仅是调整我们查询的顺序。我们得到了M个查询,我们将把查询以一个特定的顺序进行重新排序,然后处理它们。显然,这是一个离线算法。每个查询都有L和R,我们称其为“起点”和“重点”。让我们将给定的输入数组分为 N \sqrt N N 块,每一块的大小为 N / N = N N/\sqrt N = \sqrt N N/N =N ,每个“起点”落入其中的一块。每个“终点“也落入其中的一块。
  • 如果某查询的“起点”落在第p块中,则该查询属于第p块,该算法将处理第1块中的查询,然后处理第2块中的查询,等等,最后直到第 N \sqrt N N 块。我们已经有个顺序、查询按照所在的块升序排列。可以有很多的查询属于同一块。
  • 从现在开始,我会忽略(其他,译者注,所有括号内斜同)所有的块,只关注我们如何询问和回答第1块。我们将对所有块做同样的事。(第1块中的)所有查询的”起点“属于第1块,但”终点”可以在包括第1块在内的任何块中。现在让我们按照R值圣墟的顺序重新排列这些查询。我们也在块中做这个操作。(指每个块块内按R升序排序)
最终的排序是怎样的?
  • 所有的询问首先按照所在块的编号升序排列(所在块的编号是指询问的“起点”属于的块)。如果编号相同,则按R值升序排序。
  • 例如考虑如下的询问,假设我们会有3个大小为3的块(0-2,3-5,6-8):
  • {0,3}{1,7}{2,8}{7,8}{4,8}{4,4}{1,2}
  • 让我们现根据所在块的编号重新排列它们
  • {0,3}{1,7}{2,8}{1,2} | {4,8}{4,4} | {7,8}(按照左值排列并分区)
  • 现在我们按照R值重新排列
  • {1,2}{0,3}{1,7}{2,8} | {4,4}{4,8} | {7,8}(按照分区中的右值排列并分区)
  • 现在我们使用与上一节所述相同的代码来解决这个问题。上述算法是正确,因为我们没有做任何改变,只是重新排列了查询的顺序。
莫队算法 模板
#include<cstdio>
#include<algorithm>
using namespace std;

#define N 311111
#define A 1111111
#define BLOCK 555//~sqrt(N)

int cnt[A],a[N],ans[N],answer = 0;

struct node{
    int L,R,i;
}q[N];

bool cmp(node x,node y){
    if(x.L/BLOCK!=y.L/BLOCK){
        //different blocks, so sort by block
        return x.L/BLOCK < y.L/BLOCK;
    }
    //same block, so sort by R value
    return x.R<y.R;
}

void add(int position){
    cnt[a[position]]++;
    if(cnt[a[position]]==1){
        answer++;
    }
}

void remove(int position){
    cnt[a[position]]--;
    if(cnt[a[position]]==0){
        answer--;
    }
}

int main(){
    int n;
    scanf("%d",&n);
    for(int i = 0;i < n;++i){
        scanf("%d",&a[i]);
    }

    int m;
    scanf("%d",&m);
    for(int i = 0;i < m;++i){
        scanf("%d%d",&q[i].L,&q[i].R);
        q[i].L--;q[i].R--;
        q[i].i = i;
    }

    sort(q,q+m,cmp);

    int currentL = 0,currentR = 0;
    for(int i = 0;i <m;++i)
    {
        int L = q[i].L,R = q[i].R;
        while(currentL < L){
            remove(currentL);
            currentL++;
        }
        while(currentL > L){
            add(currentL-1);
            currentL--;
        }
        while(currentR <= R){
            add(currentR);
            currentR++;
        }
        while(currentR > R+1){
            remove(currentR-1);
            currentR--;
        }
        ans[q[i].i] = answer;
    }
    for(int i = 0;i < m;++i)
        printf("%d\n",ans[i]);

    return 0;
}

例题1:D-Query(模板题)

https://www.spoj.com/problems/DQUERY/en/

#include<cstdio>
#include<algorithm>
using namespace std;

#define N 311111
#define A 1111111
#define BLOCK 555//~sqrt(N)

int cnt[A],a[N],ans[N],answer = 0;

struct node{
    int L,R,i;
}q[N];

bool cmp(node x,node y){
    if(x.L/BLOCK!=y.L/BLOCK){
        //different blocks, so sort by block
        return x.L/BLOCK < y.L/BLOCK;
    }
    //same block, so sort by R value
    return x.R<y.R;
}

void add(int position){
    cnt[a[position]]++;
    if(cnt[a[position]]==1){
        answer++;
    }
}

void remove(int position){
    cnt[a[position]]--;
    if(cnt[a[position]]==0){
        answer--;
    }
}

int main(){
    int n;
    scanf("%d",&n);//n个数字
    for(int i = 0;i < n;++i){
        scanf("%d",&a[i]);
    }

    int m;
    scanf("%d",&m);//m组查询
    for(int i = 0;i < m;++i){
        scanf("%d%D",&q[i].L,&q[i].R);//查询的左右取区间
        q[i].L--;q[i].R--;
        q[i].i = i;
    }

    sort(q,q+m,cmp);

    int currentL = 0,currentR = 0;
    for(int i = 0;i <m;++i)
    {
        int L = q[i].L,R = q[i].R;
        while(currentL < L){
            remove(currentL);
            currentL++;
        }
        while(currentL > L){
            add(currentL-1);
            currentL--;
        }
        while(currentR <= R){
            add(currentR);
            currentR++;
        }
        while(currentR > R+1){
            remove(currentR-1);
            currentR--;
        }
        ans[q[i].i] = answer;
    }
    for(int i = 0;i < m;++i)
        printf("%d\n",ans[i]);

    return 0;
}

例题2:codeforce-86D 略

例题3:codeforce-617E

这题其实相当不好想

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值