莫队(离线处理区间询问)

我第一次听说莫队这个算法是好久好久以前了,我一直认为这是一个比较难的算法,所以从未尝试去学习他,直到我今天看了莫队,我才知道莫队其实就是一个利用分块思想对暴力解决区间询问问题的一个优化。

下面我将一步步分析莫队的优化过程,我先来引入一个例子:

莫队(离线处理区间询问)

 这道问题我们应该怎么解决呢?一开始肯定是暴力的做法了,我们可以初始定义一个l,r,并计算出区间[ l , r ]的答案,不妨假设下一次询问是[ l - p , r - q ],那么我们就可以左移l,每次左移一次就加上l的贡献,左移r,每次左移就减去r的贡献。这样算到最后就能得到区间[ l - p , r - q ]的答案。但是这样会有一个问题:就是区间询问如果是两个询问之间距离差距较大的话,这样复杂度是很高的,比如(1,10),(10000,10008),(2,15),(10050,10100)……,这样的区间,l从1移动到10000再移动到2,再移动到10050,每次跨度都很大,这样复杂度巨高。

这时候肯定有同学会想到这样的解决方法,就是按照每个询问的l为第一优先级,r为第二优先级进行从小到大排序,然后再按照之前的暴力思路进行做。

这的确是一个很好的优化思路,但是还是有数据可以卡住这种做法,比如(1,15),(100,200),(2,50),(1,5000),(2,4000),(3,3000),如果我们按照每个询问的l为第一优先级,r为第二优先级进行从小到大排序,排完序后的询问是(1,15),(1,5000),(2,50),(2,4000),(3,2000),(100,5000).这样的话l的移动顺序是从1->2->3->100,l一共是99次,而r的移动顺序是15->5000->50->4000->2000->5000,这样r一共是移动4985+4950+3950+2000+3000=18885次,显然r移动的次数太多,而如果我们按照下面这样的询问顺序进行询问:

(1,15),(2,50),(3,2000),(2,4000),(1,5000),(100,5000),这样的话l的移动顺序是1->2->3->2->1->100,移动次数是103次,r的移动顺序是15->50->2000->4000->5000->5000,r的移动次数一共是4985次,这样下面这种方法移动次数显然小于上面的移动次数。那下面这种方法就是利用了莫队的思想进行的优化。

那莫队究竟是怎样的思想呢,就是先对询问的左边界进行分块,然后按照询问左边界所在的块作为第一优先级,询问的右边界作为第二优先级进行排序。再按照暴力的思想进行求解,那一般怎样分块呢?其实根据数学推导得到当块的大小为总数的开方时,这样算法的复杂度是最低的,复杂度是o(n^1.5),证明我就简单写一下吧:

假设我们将整个区间分成x块,那么每块有n/x个, l 平均每次最多移动n/x次, r 每次最多移动n次这样对于m次询问复杂度就是max(n*m/x,m*x),显然当两者相等时,该式取得最小值,也就是x=sqrt(n)

下面说一下可以利用莫队解题的题目所具有的性质:

能够利用 [l , r ]的答案o(1)算出 [ l -1 , r ], [ l +1 , r ], [ l , r -1 ], [ l , r +1 ]区间的答案。

下面是模板:(以洛谷P2709为例)

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<queue>
#include<cmath>
using namespace std;
typedef long long ll;
const int N=1e5+10;
ll pn,n,m,k,ans;
//pn是分块的大小 
struct node{
	ll id,l,r,pl;
}p[N];
bool cmp(node a,node b)
{
	//按照询问左边界所在的块为第一优先级,右边界的大小为第二优先级进行排序
	if(a.pl!=b.pl) return a.pl<b.pl;
	return a.r<b.r;
}
ll cnt[N],sum[N],a[N];
//不同题目只需改变add和sub函数即可 
void add(ll x) 
{
	ans-=cnt[x]*cnt[x];
	cnt[x]++;
	ans+=cnt[x]*cnt[x];
}
void sub(ll x)
{
	ans-=cnt[x]*cnt[x];
	cnt[x]--;
	ans+=cnt[x]*cnt[x];
}
int main()
{
	scanf("%lld%lld%lld",&n,&m,&k);
	pn=sqrt(n);
	for(int i=1;i<=n;i++)
		scanf("%lld",&a[i]);
	for(int i=1;i<=m;i++)
	{
		scanf("%lld%lld",&p[i].l,&p[i].r);
		p[i].id=i;
		p[i].pl=(p[i].l-1)/pn+1;//将询问的左边界分块 
	}
	sort(p+1,p+m+1,cmp);
	ll l=1,r=0;
	for(int i=1;i<=m;i++)
	{
		while(l<p[i].l)
		{
			sub(a[l]);
			l++;
		}
		while(l>p[i].l)
		{
			l--;
			add(a[l]);
		}
		while(r<p[i].r)
		{
			r++;
			add(a[r]);
		}
		while(r>p[i].r)
		{
			sub(a[r]);
			r--;
		}
		sum[p[i].id]=ans;
	}
	//输出答案 
	for(int i=1;i<=m;i++)
		printf("%lld\n",sum[i]);
	return 0;
}

上一篇:Feign客户端 - 超时时间配置


下一篇:k8s 微服务打包上传私库、部署、发布