【数据结构】普通莫队

介绍

莫队算法是由莫涛提出的算法,是一种处理区间问题的暴力数据结构,时间复杂度为 O ( n n ) O(n \sqrt{n}) O(nn )

问题引入

洛谷P3901

给定一个序列 A 1 , A 2 , … , A n A_1,A_2,\dots,A_n A1,A2,,An Q Q Q 个询问 ( L i , R i ) (L_i,R_i) (Li,Ri),询问 A L i , A L i + 1 , … , A R i A_{L_i},A_{L_i+1},\ldots,A_{R_i} ALi,ALi+1,,ARi 是否互不相同。

我们非常简单就能想到一个暴力做法,下标从 L i L_i Li R i R_i Ri 便利一下,直接统计一下是否互不相同,但时间复杂度直接上天: O ( n Q ) O(nQ) O(nQ)

于是我们考虑另一个方法,移动端点:
我们用一个 l l l r r r 来计算的是下标从 l l l r r r a i a_i ai 不相等的个数,这样判断一下 a n s ans ans 是否等于 r − l + 1 r-l+1 rl+1 就行了。
如图:
在这里插入图片描述
线段上的数字表示一个下标,如果我们当前处理的询问是 ( 2 , 3 ) (2,3) (2,3),下一个询问是 ( 3 , 5 ) (3,5) (3,5),那么我们要让 l + + , r + + , r + + l++,r++,r++ l++,r++,r++,如图:
在这里插入图片描述
蓝色为原来的维护区间,绿色为变化情况,黄色为新的维护的区间。
但是我们移动端点的复杂度虽然可以 O ( 1 ) O(1) O(1) 维护,但需要移动 n n n 次,时间复杂度还是 O ( n Q ) O(nQ) O(nQ)
但我们可以想到如果将每一次的移动限定在一个范围之内,时间复杂度就将大为降低。
于是神奇的莫队算法就出现了,但其本质就是优美的暴力。

原理

核心

莫队最核心的部分还是它的排序。
我们把所有的元素分成多个块(即分块),把询问按以下方法排序:
如果左端点所在块的编号不同就按左端点从小到大排序,否则按右端点从小到大排序。
为什么要怎么排序呢,这样排序的话在每一个块内, r r r 都可以一路从左扫到右,忽略不计,最坏情况下 l l l 则在块的长度内来回横跳,时间复杂度为 O ( 块的长度 × 询问次数 ) O(块的长度 \times 询问次数) O(块的长度×询问次数).
可以知道,当块的长度为 n \sqrt{n} n 时跑的很快,那么总的时间复杂度就是 ( n n ) (n \sqrt{n}) (nn )

维护

一般情况下,我们需要写两个函数来维护增加端点删除端点两个操作。
为了方便我们用 l , r l,r l,r 表示当前维护区间, L , R L,R L,R 表示需要维护的区间。
我们就从本题讲起:
del函数:
如果删除了一个点,这个区间内这个点的出现次数为0了,那么 a n s = a n s − 1 ans=ans-1 ans=ans1
因为删除了这个数,这个区间内就没有这个数了。
如果 l < L l<L l<L,我们就要将 l l l 往右挪一下,及将 l l l 这个点删除,再将 l l l 更新为 l + 1 l+1 l+1
如果 r > R r>R r>R,我们就要将 r r r 往左挪一下,及将 r r r 这个点删除,再将 r r r 更新为 r − 1 r-1 r1
代码如下:

void del(int x)
{
	ans-=(--cnt[a[x]]==0);
}
while(l<L)del(l++);
while(r>R)del(r--);

add函数:
如果加入了一个点,这个区间内这个点的出现次数为1了,那么 a n s = a n s + 1 ans=ans+1 ans=ans+1
因为加入了这个数,这个区间内就多出了一个没有出现过的数,及前面的区间中并没有这一个数。
如果 l > L l>L l>L,我们就要将 l l l 往左挪一下,及将 l − 1 l-1 l1 这个点加入,再将 l l l 更新为 l − 1 l-1 l1
如果 r < R r<R r<R,我们就要将 r r r 往右挪一下,及将 r + 1 r+1 r+1 这个点加入,再将 r r r 更新为 r + 1 r+1 r+1
注:这里是加入,及原来的这个端点还在维护范围内,所以我们加入的应该是旁边的端点,及先更新再加入,而不是先加入再更新,这一点和删除并不一样
代码如下:

void add(int x)
{
	ans+=(++cnt[a[x]]==1);
}
while(l>L)add(--l);
while(r<R)add(++r);

有了这两个函数之后,我们就可以不断维护每一个询问的区间了:

Code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+1;
int n,m,CL,CR,b[N],kn,ans[N],sum,a[N];
struct fy
{
	int l,r,k;
}C[N];
bool cmp(fy x,fy y)
{
	return (x.l/kn)==(y.l/kn)?x.r<y.r:x.l<y.l;
}
void add(int x)
{
	if(++b[a[x]]==1)
		sum++;
}
void del(int x)
{
	if(--b[a[x]]==0)
		sum--;
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(NULL),cout.tie(NULL);
	cin>>n>>m;
	kn=sqrt(n);
	for(int i=1;i<=n;i++)
		cin>>a[i];
	for(int i=1;i<=m;i++)
		cin>>C[i].l>>C[i].r,C[i].k=i;
	sort(C+1,C+m+1,cmp);
	for(int i=1;i<=m;i++)
	{
		int L=C[i].l,R=C[i].r;
		while(CL<L)del(CL++);
		while(CR>R)del(CR--);
		while(CL>L)add(--CL);
		while(CR<R)add(++CR);
		if(sum==(R-L+1))
			ans[C[i].k]=1;
	}
	for(int i=1;i<=m;i++)
		cout<<((ans[i])?"Yes\n":"No\n");
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值