莫队基础

莫队

对于一些区间问题如果已经知道 \([l,r]\) 区间问题的答案,那么可以通过加减 \(l\)\(r\) 两个节点的信息,得到 \([l+1,r]\)\([l-1,r]\)\([l,r-1]\)\([l,r+1]\),这样就可以的到所有区间查询的答案
具体做法是离线后排序,顺序处理每个询问,暴力从上一个区间的答案转移到下一个区间答案(一步一步移动即可)。
排序方法是以 \(l\) 为第一关键字划分成多个「块」 ,如果 \(l\) 在一个「块」内就按 \(r\) 从小到大排序,不然就按 \(l\) 从小到大排序
这样排序可以保证区间移动的长度最短,时间更快

时间复杂度

  • 「块」内移动,因为左端点都在一个块内,所以单词移动是 \(O(\sqrt{n})\) 因为有 \(q\) 次询问所以就是 \(q*\sqrt{n}\)
  • 「块」外移动是 \(O(\sqrt{n})\) 因为最多是 \(\sqrt{n}\) 个块,所以总复杂度是 \(O(n)\),而右指针移动跨「块」时可能会掉递增,往回走,因为「块」有\(\sqrt{n}\)个,这种行为最多有\(\sqrt{n}\)次,每次有\(O(n)\)步,所以外部的总复杂的是 \(O(n\,\sqrt{n})\)
  • 最后总复杂度就是 \(O((n+q)\,\sqrt{n})\)

P3901 数列找不同

莫队直接做,在移动时当前区间移动后信息的变化,维护区间的不同点的种类数:

  • \(l<查询的左端点\)\(l\) 往右移动,同时删除 \(l\) 这个点的信息
  • \(l>查询的左端点\)\(l\) 往左移动,同时添加 \(l\) 这个点的信息
  • \(r<查询的右端点\)\(r\) 往右移动,同时添加 \(r\) 这个点的信息
  • \(r>查询的右端点\)\(r\) 往左移动,同时删除 \(r\) 这个点的信息

添加和删除维护当前的点的种类数

  • 添加(ADD):将添加的点的出现次数\(++\),如果是第一次出现,那么就种类数\(++\)
  • 删除(SUB):将删除的点的出现次数\(--\),如果出现次数为变\(0\),那么种类术\(--\)

最后对于每一次查询,如果点的种类数正好是区间长度,那么说明每个点都互不相同

#include<bits/stdc++.h>
#define int long long
#define fore(i,a,b) for( int i=(a); i<=(b); ++i)
#define repe(i,a,b) for( int i=(a); i>=(b); --i)
using namespace std;
const int N=2e5+10;
struct Query{
	int l,r,id,ans;
}q[N];
int tot=0;
int a[N],n,m,len,mp[N];
bool cmp(Query A,Query B){
	if(A.l/len!=B.l/len)
		return A.l<B.l;
	return A.r<B.r;
}
bool out(Query A,Query B){
	return A.id<B.id;
}
void SUB(int x){
	mp[a[x]]--;
	if(mp[a[x]]==0)tot--;
	return;
}
void ADD(int x){
	mp[a[x]]++;
	if(mp[a[x]]==1)tot++;
	return;
}
signed main(){
	ios::sync_with_stdio(false);
	cin>>n>>m;
	len=sqrt(n);
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=1;i<=m;i++){
		cin>>q[i].l>>q[i].r;
		q[i].id=i;
	}
	sort(q+1,q+m+1,cmp);
	int l=1,r=0;
	for(int i=1;i<=m;i++){
		while(l<q[i].l)SUB(l++);
		while(l>q[i].l)ADD(--l);
		while(r<q[i].r)ADD(++r);
		while(r>q[i].r)SUB(r--);
		q[i].ans=(tot==(q[i].r-q[i].l+1));
	}
	sort(q+1,q+m+1,out);
	for(int i=1;i<=m;i++)cout<<(q[i].ans? "Yes":"No")<<'\n';
	return 0;
}




P2709 小B的询问

区间的移动方式和上一题一样,但是维护的是一个区间的答案,加减函数和原来不同,依然要记录每个点的出现次数

  • 加(ADD)向把和减去当前出现次数的平方,再把出现次数\(+1\),把和加回去
  • 减(SUB)类似
#include<bits/stdc++.h>
#define int long long
#define fore(i,a,b) for( int i=(a); i<=(b); ++i)
#define repe(i,a,b) for( int i=(a); i>=(b); --i)
using namespace std;
const int N=2e5+10;
struct Query{
	int l,r,id,ans;
}q[N];
int sum=0;
int a[N],n,m,k,len,mp[N];
bool cmp(Query A,Query B){
	if(A.l/len!=B.l/len)
		return A.l<B.l;
	return A.r<B.r;
}
bool out(Query A,Query B){
	return A.id<B.id;
}
void SUB(int x){
	sum-=(mp[a[x]]*mp[a[x]]);
	mp[a[x]]--;
	sum+=(mp[a[x]]*mp[a[x]]);
	return;
}
void ADD(int x){
	sum-=(mp[a[x]]*mp[a[x]]);
	mp[a[x]]++;
	sum+=(mp[a[x]]*mp[a[x]]);
	return;
}
signed main(){
	ios::sync_with_stdio(false);
	cin>>n>>m>>k;
	len=sqrt(n);
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=1;i<=m;i++){
		cin>>q[i].l>>q[i].r;
		q[i].id=i;
	}
	sort(q+1,q+m+1,cmp);
	int l=1,r=0;
	for(int i=1;i<=m;i++){
		while(l<q[i].l)SUB(l++);
		while(l>q[i].l)ADD(--l);
		while(r<q[i].r)ADD(++r);
		while(r>q[i].r)SUB(r--);
		q[i].ans=sum;
	}
	sort(q+1,q+m+1,out);
	for(int i=1;i<=m;i++)cout<<q[i].ans<<'\n';
	return 0;
}




P1494 [国家集训队] 小 Z 的袜子

和前两题类似,记录每个颜色的出现次数,维护颜色的种类数
每一次查询的概率用分数表示,分子就只对于每一种颜色的出现次数 \(x\),那么摸到两个这个颜色的概率就是\(x*(x-1)\)
把所有颜色的概率加加起来就是这次询问的分子
而分母就是区间长度\(len*(len-1)\),约分把分子分母同时除以\(gcd\)

#include<bits/stdc++.h>
#define int long long
#define fore(i,a,b) for( int i=(a); i<=(b); ++i)
#define repe(i,a,b) for( int i=(a); i>=(b); --i)
using namespace std;
const int N=2e5+10;
struct Query{
	int l,r,id,ans;
}q[N];
int a[N],n,m,len;
int sum;
int mp[N];
bool cmp(Query A,Query B){
	if(A.l/len!=B.l/len)
		return A.l<B.l;
	return A.r<B.r;
}
bool out(Query A,Query B){
	return A.id<B.id;
}
void SUB(int x){
	sum-=(mp[a[x]]*(mp[a[x]]-1));
	mp[a[x]]--;
	if(mp[a[x]]>0)sum+=(mp[a[x]]*(mp[a[x]]-1));
	return;
}
void ADD(int x){
	if(mp[a[x]]>0)sum-=(mp[a[x]]*(mp[a[x]]-1));
	mp[a[x]]++;
	sum+=(mp[a[x]]*(mp[a[x]]-1));
	return;
}
int gcd(int x,int y){
	if(!y)return x;
	return gcd(y,x%y);
}
signed main(){
	ios::sync_with_stdio(false);
//	freopen("P1494.in","r",stdin);
	cin>>n>>m;
	len=sqrt(n);
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=1;i<=m;i++){
		cin>>q[i].l>>q[i].r;
		q[i].id=i;
	}
	sort(q+1,q+m+1,cmp);
	int l=1,r=0;
	for(int i=1;i<=m;i++){
		if(q[i].l==q[i].r){
			q[i].ans=0;
			continue;
		}
		while(l<q[i].l)SUB(l++);
		while(l>q[i].l)ADD(--l);
		while(r<q[i].r)ADD(++r);
		while(r>q[i].r)SUB(r--);
		q[i].ans=sum;
	}
	sort(q+1,q+m+1,out);
	for(int i=1;i<=m;i++){	
		if(q[i].l==q[i].r){
			cout<<0<<'/'<<1<<'\n';
			continue;
			
		}
		int a=q[i].ans;
		int b=(q[i].r-q[i].l+1)*(q[i].r-q[i].l);
		cout<<a/(gcd(a,b))<<'/'<<b/gcd(a,b)<<'\n';
	}
	return 0;
}




P4462 [CQOI2018] 异或序列

计算异或前缀和,一段区间的异或和\(sum(l,r)\)就是\(val[r] \oplus val[l-1]\)
题目需要\(sum(l,r)==k\) 也就是

\[val[r] \oplus val[l-1]==k \]

\(val[l-1]\)移到右边

\[val[l-1]==k \oplus val[r] \]

这样就可以转化成和之前一样的统计每一个 \(val[i]\) 的值的出现次数对于每一个
\(val[r]\)符合条件的数的个数,就是\(val[r] \oplus k\) 这个值的出现次数

#include<bits/stdc++.h>
#define int long long
#define fore(i,a,b) for( int i=(a); i<=(b); ++i)
#define repe(i,a,b) for( int i=(a); i>=(b); --i)
using namespace std;
const int N=1e5+10;
int n,m,k,len,sum;
int a[N],val[N];
int cnt[N];
struct Query{
	int l,r,id,ans;
}q[N];
bool cmp(Query A,Query B){
	if(A.l/len!=B.l/len)
		return A.l<B.l;
	return A.r<B.r;
}
bool out(Query A,Query B){
	return A.id<B.id;
}
void ADD(int x){
	sum+=cnt[val[x]^k];
	cnt[val[x]]++;
	return;
}
void SUB(int x){
	cnt[val[x]]--;
	sum-=cnt[val[x]^k];
	return;
}
signed main(){
	ios::sync_with_stdio(false);
	cin>>n>>m>>k; 
	len=sqrt(n);
	for(int i=1;i<=n;i++){
		cin>>a[i];
		val[i]=val[i-1]^a[i];
	}
	for(int i=1;i<=m;i++){
		cin>>q[i].l>>q[i].r;
		q[i].id=i;
	}
	sort(q+1,q+m+1,cmp);
	int l=1,r=0;
	cnt[0]=1;
	for(int i=1;i<=m;i++){
		while(l<q[i].l){SUB(l-1);l++;}
		while(l>q[i].l){l--,ADD(l-1);}
		while(r<q[i].r)ADD(++r);
		while(r>q[i].r)SUB(r--);
		q[i].ans=sum;
	}
	sort(q+1,q+m+1,out);
	for(int i=1;i<=m;i++){
		cout<<q[i].ans<<'\n';
	}
	return 0;
}




posted @ 2025-08-04 16:33  wmq2012  阅读(5)  评论(0)    收藏  举报