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

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

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

 这道问题我们应该怎么解决呢?一开始肯定是暴力的做法了,我们可以初始定义一个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;
}

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
莫队算法是一种基于分块的算法,用于解决一些静态区间询问题,时间复杂度为 $O(n\sqrt{n})$。以下是一个基于Python的莫队算法的示例代码: ```python import math # 定义块的大小 BLOCK_SIZE = 0 # 初始化块的大小 def init_block_size(n): global BLOCK_SIZE BLOCK_SIZE = int(math.sqrt(n)) # 定义查询操作 def query(left, right): pass # 在这里写查询操作的代码 # 定义添加操作 def add(x): pass # 在这里写添加操作的代码 # 定义删除操作 def remove(x): pass # 在这里写删除操作的代码 # 定义莫队算法 def mo_algorithm(n, q, queries): init_block_size(n) queries.sort(key=lambda x: (x[0] // BLOCK_SIZE, x[1])) left, right = 0, -1 for query in queries: while left > query[0]: left -= 1 add(left) while right < query[1]: right += 1 add(right) while left < query[0]: remove(left) left += 1 while right > query[1]: remove(right) right -= 1 query(query[0], query[1]) ``` 在这段代码中,我们首先定义了一个全局变量 `BLOCK_SIZE`,用于表示块的大小。接着,我们定义了三个操作函数 `query()`、`add()` 和 `remove()`,分别用于查询、添加和删除元素。在 `mo_algorithm()` 函数中,我们首先调用 `init_block_size()` 函数初始化块的大小,然后将查询操作按照块的大小和右端点排序,接着使用双指针维护当前查询区间的左右端点,每次移动指针时调用 `add()` 和 `remove()` 函数更新块的状态,最后调用 `query()` 函数进行查询操作。 请注意,这段代码只是一个示例,具体的 `query()`、`add()` 和 `remove()` 函数的实现取决于具体的问题

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值