普通莫队

普通莫队

形式

如果从\([l,r]\) 的答案能够$ O(1)$扩展到 \([l+1,r][l-1,r][l,r+1][l,r-1]\)(即与\([l,r]\)相邻的区间)的答案,那么使用莫队算法可以在\(O(n\sqrt n)\)的复杂度内求出所有询问的答案。

核心

我们假设已经知道\([l,r]\)的答案,现在我们要求\([l',r']\),我们就可以移动左右指针,从而计算出答案

但是这可以被卡到\(O(n)\)

因此我们考虑进行离线处理

我们对\(l\)的数值进行分块,一共分成\(\sqrt n\)段,每一段的编号相同

第一关键字是段的编号,第二关键字是右端点的编号

然后我们就可以暴力了

时间复杂度

前提条件:m与n同阶

实际上莫队我们可以理解为左端点牺牲了一定的有序,换取了时间复杂度的优化

如果我们严格按照左端点不降,相同时比较右端点,这时如果相邻两个右端点相差n时,时间复杂度就是\(O(n^2)\)

如果我们用分块的思想,将一定量的左端点笼统的分为一类,比如我们每\(\sqrt {n}\)分为一块,块内的按照右端点排序

这样对于每一个新块进行第一次计算,时间复杂度相当于\(O(n)\),总时间为\(O(n\sqrt {n})\)

对于块内后续的计算,我们的右端点已经有序,时间复杂度相当于\(O(n)\),总时间为\(O(n\sqrt {n})\)

但这时我们的左端点并不是有序的,我们设每一块内的左端点的最大值为\([max_1,max_2,...,max_{(\sqrt {n})}]\)

我们每一次最多可能移动\(max_i-max_{i-1}\)

因此我们有

\[\begin{aligned} &O(\sqrt {n} (max_1-0)+\sqrt {n} (max_2-max_1)+...+\sqrt {n} (max_{\sqrt{n}}-max_{\sqrt{n}-1}))\\ =&O(\sqrt{n}(max_1-0+max_2-max_1+...+max_{\sqrt{n}}-max_{\sqrt{n}-1}))\\ =&O(\sqrt{n}\cdot max_{\sqrt{n}})\\ =&O(n\sqrt{n}) \end{aligned} \]

综上,时间复杂度为\(O(n\sqrt{n})\)

参考普通莫队算法 - OI Wiki (oi-wiki.org)

细节问题

1,\(l\)要为1,\(r\)要为0

2,分块时注意第一关键字,分不好就会TLE

3,询问间区间的转移,我们最好先扩大区间,再缩小区间,避免出现问题

2339 Toys "R" Us - PCOI Online Judge (pcoij8.ddns.net)

题目大意

询问\([l,r]\)中第一个没有出现的正整数

做法

一眼莫队,对于寻找第一个没出现的正整数,我们考虑用set查询

代码

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
int cnt[maxn],a[maxn],ans[maxn];
set<int>Set;
int n,m,l,size;
struct node{
	int x,y,id;
}q[maxn];
bool cmp(node x,node y){
	if(x.x/size==y.x/size)return x.y<y.y;
	return x.x/size<y.x/size;
}
void update(int now,int whi){
	if(whi==1){
		cnt[a[now]]++;
		if(cnt[a[now]]==1){
			Set.erase(a[now]);
		}
	}
	else{
		cnt[a[now]]--;
		if(!cnt[a[now]])
			Set.insert(a[now]);
	}
}
int main(){
	for(int i=1;i<=100001;i++)Set.insert(i);
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
	}
	for(int i=1;i<=m;i++){
		scanf("%d%d",&q[i].x,&q[i].y);
		q[i].id=i;
	}
	size=sqrt(m);
	sort(q+1,q+m+1,cmp);
	int l=1,r=0;
	for(int i=1;i<=m;i++){
		int ql=q[i].x,qr=q[i].y;
		while(r<qr)update(++r,1);
		while(r>qr)update(r--,-1);
		while(l>ql)update(--l,1);
		while(l<ql)update(l++,-1);
		ans[q[i].id]=*Set.begin();
	}
	for(int i=1;i<=m;i++){
		printf("%d\n",ans[i]);
	}
	
	
	return 0;
}

DQUERY - D-query - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

题目大意

求区间内不同的数的个数

做法

板子

代码

#include<bits/stdc++.h>
using namespace std;
const int maxn=3*1e4+5,maxm=1e6+5;
int size,n,m,num[maxn],d[maxm],NUM,ans[maxm]; 
struct node{
	int x,y,id;
}a[maxm];
bool cmp(node x,node y){
	if(x.x/size==y.x/size)return x.y<y.y;
	return x.x<y.x;
}
void update(int now){
	if(!d[num[now]])NUM++;
	d[num[now]]++;
}
void del(int now){
	d[num[now]]--;
	if(!d[num[now]])NUM--;
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>num[i];
	}
	cin>>m;
	for(int i=1;i<=m;i++){
		cin>>a[i].x>>a[i].y;
		a[i].id=i;
	}
	size=sqrt(n);
	sort(a+1,a+m+1,cmp);
	int l=1,r=0;
	for(int i=1;i<=m;i++){
		int L=a[i].x,R=a[i].y;
		while(r+1<=R)update(++r);
		while(L<=l-1)update(--l);
		while(r>R)del(r--);
		while(l<L)del(l++);
		ans[a[i].id]=NUM;
	}
	for(int i=1;i<=m;i++)cout<<ans[i]<<endl;
	return 0;
}

[P1494 国家集训队] 小 Z 的袜子 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

做法

和前面类似,改一下update和del就可以了

代码

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=5*1e4+5;
int size=0,num[maxn],tot,d[maxn],n,m;
pair<int,int>ans[maxn];
struct node{
	int x,y,id;
}a[maxn];
bool cmp(node x,node y){
	if(x.x/size==y.x/size)return x.y<y.y;
	return x.x/size<y.x/size;
}
void update(int now){
	now=num[now];
	tot-=d[now]*(d[now]-1)/2;
	d[now]++;
	tot+=d[now]*(d[now]-1)/2;
	return ;
}
void del(int now){
	now=num[now];
	tot-=d[now]*(d[now]-1)/2;
	d[now]--;
	tot+=d[now]*(d[now]-1)/2;
	return ;
}
signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>num[i];
	}
	for(int i=1;i<=m;i++){
		cin>>a[i].x>>a[i].y;
		a[i].id=i;
	}
	size=sqrt(n);
	sort(a+1,a+m+1,cmp);
	int l=1,r=0;
	
	for(int i=1;i<=m;i++){
		int L=a[i].x,R=a[i].y;
		while(r+1<=R)update(++r);
		while(L<=l-1)update(--l);
		while(r>R)del(r--);
		while(l<L)del(l++);
		int A=tot,B=(R-L+1);
		B=B*(B-1)/2;
		if(B!=0){
			int C=__gcd(A,B);
			A/=C,B/=C;
		}
		else A=0,B=1;
		ans[a[i].id]={A,B};
	}
	for(int i=1;i<=m;i++){
		printf("%lld/%lld\n",ans[i].first,ans[i].second);
	}
	return 0;
}

P3709 大爷的字符串题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

题目大意

求区间中众数出现的次数

做法

莫队,我们先离散化,然后每一次记下该数出现的次数

我们再开一个数组记录出现i次的数的个数

对于扩大区间我们可以比较容易的更新答案

如果缩小区间,我们看一下减少的 这个数的 出现次数是不是最大,再判断出现次数最大的数 的个数是否只有一个,如果是的话答案就要减一

代码

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=2*1e5+5;
int size=0,num[maxn],tot,n,m,t[maxn],d[maxn];
map<int,int>mp;
int ans[maxn],ANS;
struct node{
	int x,y,id;
}a[maxn];
bool cmp(node x,node y){
	if(x.x/size==y.x/size)return x.y<y.y;
	return x.x/size<y.x/size;
}
void update(int now){
	now=num[now];
	d[now]++;
	t[d[now]-1]--;
	t[d[now]]++;
	ANS=max(ANS,d[now]);
	return ;
}
void del(int now){
	now=num[now];
	if(d[now]==ANS&&t[d[now]]==1)ANS--;
	t[d[now]-1]++;
	t[d[now]]--;
	d[now]--;
	return ;
}
signed main(){
	cin>>n>>m;
	t[0]=n;
	for(int i=1;i<=n;i++){
		cin>>num[i];
		mp[num[i]]=1;
	}
	int len=0;
	for(map<int,int>::iterator it=mp.begin();it!=mp.end();it++)it->second=++len;
	for(int i=1;i<=n;i++){
		num[i]=mp[num[i]];
	}
	for(int i=1;i<=m;i++){
		cin>>a[i].x>>a[i].y;
		a[i].id=i;
	}
	size=sqrt(n);
	sort(a+1,a+m+1,cmp);
	int l=1,r=0;
	
	for(int i=1;i<=m;i++){
		int L=a[i].x,R=a[i].y;
		while(r+1<=R)update(++r);
		while(L<=l-1)update(--l);
		while(r>R)del(r--);
		while(l<L)del(l++);
//		cout<<L<<" "<<R<<" "<<ANS<<endl;
		ans[a[i].id]=ANS;
	}
	for(int i=1;i<=m;i++)cout<<-ans[i]<<endl;
	return 0;
}

带修改莫队

普通莫队是不能带修改的。

我们可以强行让它可以修改,就像 DP 一样,可以强行加上一维 时间维, 表示这次操作的时间。

时间维表示经历的修改次数。

即把询问\([l,r]\)变成\([l,r,time]\)

时间复杂度

我们设序列长为n,m个询问,t个修改。

带修莫队排序的第二关键字是右端点所在块编号,不同于普通莫队。

想一想,如果不把右端点分块:

  • 乱序的右端点对于每个询问会移动n次。
  • 有序的右端点会带来乱序的时间,每次询问会移动t次。

当n,m,t同阶的时候,时间复杂度我们可以看作\(O(n^{\frac{5}{3}})\)

我们一般使用\(n^{\frac{2}{3}}\)来当块长

具体地参考:带修改莫队 - OI Wiki (oi-wiki.org)

[P1903 国家集训队] 数颜色 / 维护队列 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

题目大意

找区间中有多少个不同的数,有修改操作

做法

我们开两个数组,第一个是记录询问,第二个是记录修改

我们每遇到一个修改操作,我们就把时间戳加一

我们按左端点块的编号,右端点块的编号,时间戳来排序

我们查询时,先按照前面一样,把指针指向现在的l,r,然后我们再在时间轴上跳,

如果修改的数不在区间内,那就直接修改,否则我们要更新一下答案

写代码的时候,我们可以记录修改的位置与数,当我们修改的时候,我们直接把数和原数组相应的位置swap一下就可以了

代码

#include<bits/stdc++.h>
using namespace std;
const int maxn=133333+5,maxm=1e6+5;
int size;
int f[maxn],d[maxm],ANS=0,n,m,B,C,lena=0,ans[maxn];
char A;
struct node1{
	int x,y,t,id;
}a[maxn];
struct node2{
	int pos,num;
}b[maxn];
bool cmp(node1 x,node1 y){
	if(x.x/size==y.x/size){
		if(x.y/size==y.y/size)return x.t<y.t;
		return x.y<y.y;
	}
	return x.x<y.x;
}
void update(int now){
	now=f[now];
	if(!d[now])ANS++;
	d[now]++;
}
void del(int now){
	now=f[now];
	d[now]--;
	if(!d[now])ANS--;
}
void change(int now,int l,int r){
	if(l<=b[now].pos&&b[now].pos<=r){
		del(b[now].pos);
		swap(b[now].num,f[b[now].pos]);
		update(b[now].pos);
	}
	else swap(b[now].num,f[b[now].pos]);
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>f[i];
	}
	int T=0;
	for(int i=1;i<=m;i++){
		cin>>A>>B>>C;
		if(A=='Q'){
			if(B>C)swap(B,C);
			++lena;
			a[lena]={B,C,T,lena};
		}
		else{
			b[++T]={B,C};
		}
	}
	size=pow(n,double(2)/double(3));
	sort(a+1,a+lena+1,cmp);

	int l=1,r=0;T=0;
	for(int i=1;i<=lena;i++){
		int L=a[i].x,R=a[i].y,Time=a[i].t;
//		cout<<L<<" "<<R<<" "<<Time<<endl;
		while(r+1<=R)update(++r);
		while(L<=l-1)update(--l);
		while(R<r)del(r--);
		while(l<L)del(l++);
//		cout<<ANS<<endl;
		while(T+1<=Time)change(++T,l,r);
		while(Time<T)change(T--,l,r);
		ans[a[i].id]=ANS;
//		cout<<ANS<<endl;
	}
	for(int i=1;i<=lena;i++)cout<<ans[i]<<endl;
	return 0;
}
posted @ 2023-07-24 17:05  Ayaka_T  阅读(25)  评论(0)    收藏  举报