莫队算法

莫队算法

摘要

莫队算法大概有基础莫队、树形莫队、带修(带修改的)莫队等类型,本文仅介绍基础莫队。可以看作莫队算法的入门。
莫队算法据说是2010年国家集训队的莫涛发明的一个优化类的算法,由于莫涛经常打比赛做队长,大家都叫他莫队,该算法也被称为莫队算法。它使用指针移动以及分块的思想对朴素算法进行优化。
莫队算法是离线算法,可以解决一类离线的区间问题,对于序列上的区间询问问题,如果从 [l,r] 的答案能够 O(1) 扩展到 [l−1,r],[l+1,r],[l,r+1],[l,r−1] 的答案,那么可以在 O(n√n) 的复杂度内求出所有询问的答案。

问题描述

SPOJ D-query: 给定一个数组,每次询问一个区间内有多少个不同的元素。
解题思路:

  1. 朴素思想
    注意到询问没有强制在线,因此我们可以使用离线的方法。首先我们考虑此类问题,如果我们已经计算出[L , R]的答案以及中间结果,那么我们显然可以在常数时间内计算出[L-1,R] , [L+1 , R] , [L , R-1] , [L , R+1]的答案,即便所给的询问并不一定是这样相差为1的区间,但这也启发了我们要尽可能将区间范围相近的放在一起计算。
  2. 为什么将询问区间相近的放在一起计算可以节省时间呢?
    因为我们通过两个指针(p和q)的左右移动来统计区间信息,显然对于程序的每次运行,这两个指针的移动次数越少越好,要想移动的尽量少,显然区间相近的放在一起最好。
  3. 如何排序?
    按照以上思路,我们要做的就是对区间进行排序,使得范围相近的询问区间尽量放在一起;但是区间有两个关键字(左端点和右端点),如果我们按照左端点严格升序,再按照右端点严格升序,那么很容易就想到一些反例来证明这种方法不是最优的,例如:(1, 100), (2, 2), (3, 99), (4, 4), (5, 102), (6, 7)。显然严格升序是不好的,很容易被出题人针对,如果能适当的减少右端点的移动次数,那么即使左端点的移动次数增加些许也是可以接受的。因此我们就要设计一种均衡的算法,使得左右端点并不一定严格有序,但总体复杂度(指针移动次数)尽量小。
    利用分块思想,我们可以实现上述目标。我们将长度为n的序列分为长度为 n \sqrt{n} n 的若干块,将区间按照左端点所在的块的序号进行排序,如果块号相同则按照右端点排序。 当然我们只是概念上分块,并不一定需要严格存储块。之后我们只需要按照排序好是顺序挨个计算即可。

总结起来一共三步:分块、排序、计算。算法复杂度为 O ( N N ) O(N \sqrt{N}) O(NN )

结束语

可以发现本文是应用型的,对于算法的正确性证明以及复杂度证明都没有涉及,这方面的原因之一是由于这是入门文章,写太多证明反而令初学者害怕,如果学有余力自然可以找相关证明;然后就是拓展,掌握了基本思想再去拓展就轻松很多了,拓展方向可以向树形莫队、带修莫队等常见类型拓展,同时注意比较莫队和其他数据结构的异同以及优劣。

代码示例
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<queue>
using namespace std;
const int N = 31000;
const int Q = 2e5+10;
int n,a[N],m;
int ans,sum[1010000];//一个全局答案ans和标记数组sum是必要的 
//当然也可以离散化 
int len;	//块大小,在读入询问前需要先赋值 
struct Query{
	int l,r,block,id;
	Query(){}
	Query(int l,int r,int id):l(l),r(r),id(id){
		block = l/len;
	}
	bool operator <(const Query &B) const{
		if(block == B.block) return block&1? r < B.r : r > B.r;
		//小优化,使得r呈波浪形 
		return block < B.block;
	}
}query[Q];
priority_queue<pair<int,int> > q;
//增加或减少一个x位置上的数 
void Move(int x,int v){
	x = a[x]; 
	sum[x] += v;
	if(v > 0) ans += sum[x] == 1;//判等是为了防止重复计数 
	else ans -= sum[x] == 0; 
}
int main(){
	scanf("%d",&n);
	len = sqrt(n);
	for(int i = 1;i <= n;i++) scanf("%d",a+i);
	scanf("%d",&m);
	for(int i = 1,x,y;i <= m;i++){
		scanf("%d%d",&x,&y);
		query[i] = Query(x,y,i);
	}
	sort(query+1,query+1+m);
	int l = 1,r = 0;
	for(int i = 1;i <= m;i++){
		while(l < query[i].l) Move(l,-1),l++;
		while(l > query[i].l) Move(l-1,1),l--;
		while(r > query[i].r) Move(r,-1),r--;
		while(r < query[i].r) Move(r+1,1),r++;
		q.push(make_pair(-query[i].id,ans));//大根堆变为小根堆 
	}
	while(!q.empty()){
		int x = q.top().second;q.pop();
		printf("%d\n",x);
	}
	return 0;
}
参考资料

[1] 张瑯小强的博客,2019.7.4
[2] 例题测试地址SPOJ-DQUERY

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

迷亭1213

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值