Loading

笔记·普通莫队

笔记·莫队

形式

假设 \(n=m\),那么对于序列上的区间询问问题,如果从 \([l,r]\) 的答案能够 \(O(1)\) 扩展到 \([l-1,r],[l+1,r],[l,r+1],[l,r-1]\)(即与 \([l,r]\) 相邻的区间)的答案,那么可以在 \(O(n\sqrt{n})\) 的复杂度内求出所有询问的答案。

如何

考虑将询问离线后排序。

我们将要询问的区间分块,每个块的大小为 \(\Theta(\sqrt{n})\)

将询问按照 \(l\) 所在的块的编号从大到小为第一关键字,\(r\) 从小到大为第二关键字排序,按照上面的方式扩展。

这样扩展下来的复杂度是 \(O(n\sqrt{n})\) 的(假设 \(n,q\) 同阶)。对左右端点分别考虑:

  1. 左端点会移动 \(O(q\sqrt{n})\) 次。因为我们按照 \(l\) 所在的块进行了排序,所以对于每个询问,\(l\) 会移动 \(\sqrt{n}\) 次。
  2. 右端点会移动 \(O(n\sqrt{n})\) 次。对于每个块,\(r\) 都是从小到大的。那么对于每个块,\(r\) 会移动 \(O(n)\) 次,最多有 \(O(n\sqrt{n})\) 个块,所以这部分最多移动 \(O(n\sqrt{n})\) 次。

代码看题。

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

题意

给定一个长度为 \(n\) 的序列 \(a\)\(q\) 次询问,每次查询 \([l,r]\) 中相等的元素对数。

原本是求概率,实际上直接除上 \(\frac{n(n-1)}{2}\) 即可。

做法

考虑如何扩展。

显然,开一个桶,记录一个元素的出现次数即可。那么加入一个元素就会产生原有元素个数的贡献,删除元素就会失去删掉这个数之后的元素个数的贡献。

代码

const int maxn=5e4+10;
int n,m;
int B;
int a[maxn];
int buc[maxn];
int ans[2][maxn];
struct query{//用一个结构体存询问
	int l,r,id;
	bool operator < (const query cmp)const{//对询问排序
		if(l/B==cmp.l/B)return r<cmp.r;
		return l/B<cmp.l/B;
	}
}q[maxn];
void solve(){//莫队
	int l=1,r=1;
	int tmp=0;
	buc[a[1]]=1;
	for(int i=1;i<=m;i++){
		while(l!=q[i].l||r!=q[i].r){
			if(r<q[i].r){
				++r;
				tmp+=buc[a[r]];
				++buc[a[r]];
			}
			if(l>q[i].l){
				--l;
				tmp+=buc[a[l]];
				++buc[a[l]];
			}
			if(l<q[i].l){
				--buc[a[l]];
				tmp-=buc[a[l]];
				++l;
			}
			if(r>q[i].r){
				--buc[a[r]];
				tmp-=buc[a[r]];
				--r;
			}
		}
		ans[0][q[i].id]=tmp;
		ans[1][q[i].id]=((q[i].r-q[i].l+1)*(q[i].r-q[i].l+1)-(q[i].r-q[i].l+1))>>1;
	}
}
signed main(){
	n=read(),m=read();
	B=sqrt(n)+1;
	for(int i=1;i<=n;i++)a[i]=read();
	for(int i=1;i<=m;i++){
		q[i].l=read();
		q[i].r=read();
		q[i].id=i;
	}
	sort(q+1,q+1+m);//对询问排序
	solve();//莫队
	for(int i=1;i<=m;i++){
		if(ans[1][i])cout<<ans[0][i]/__gcd(ans[0][i],ans[1][i])<<'/'<<ans[1][i]/__gcd(ans[0][i],ans[1][i])<<'\n';//分数化简
		else puts("0/1");//根据题意,特判
	}
	return 0;
}

P3901 数列找不同

题意

给定一个长度为 \(n\) 的序列 \(a\)\(q\) 次询问,每次查询 \([l,r]\) 元素是否两两不同。

做法

考虑到两两不同就是相同元素对数为 \(0\)

所以,直接把上一题的代码稍加改动即可。

代码

贺代码即可。

CF877F Ann and Books

题意

商店里有 \(n\) 本书,每本书中有 \(a_i\)\(t_i=1/2\) 类问题。

\(q\) 次询问,每次询问给出一个区间,求有多少个符合下列条件的区间:

  • 这个区间是给出区间的子区间
  • 这个区间的所有书中第 \(1\) 类问题比第 \(2\) 类问题多 \(m\) 个,其中 \(m\) 在所有询问中相同。

\(n,q\le 10^5\)

做法

不好直接扩展,考虑求出前缀和。

\(1\) 类问题视为 \(1\)\(2\) 类问题视为 \(-1\)。记前缀 \([1,i]\) 和为 \(pre_i\),那么一个区间 \([l,r]\) 中的 \(1,2\) 类问题数量之差即为 \(pre_r-pre_{l-1}\)

问题转化为:\(pre_r-pre_{l-1}\) 的子区间 \([l,r]\) 的数量。

这个就相当好求了。开两个桶,\(buc_i\) 记录值为 \(i\)\(pre_{j-1}\) 数量,\(buc2_i\) 记录值为 \(i\)\(pre_{j}\) 数量。

当左端点扩展时,考虑用 \(pre_{l-1}\)\(buc2\) 计算贡献;当右端点进行扩展时,考虑使用 \(pre_r\)\(buc\) 计算贡献。

不难发现,前缀和的值域可以飙到 \(10^{15}\),这是不好处理的,考虑对 \(pre_i,pre_i+m,pre_i-m\) 进行离散化。注意,因为计算 \(pre_{l-1}\) 时,\(l\) 可能等于 \(1\),所以也要对 \(i=0\) 进行离散化。

细节见代码。

代码

const int maxn=1e5+10;
int n,m,q;
int B;
struct query{
	int l,r,id;
	bool operator < (const query cmp)const{
		if(l/B==cmp.l/B)return r<cmp.r;
		return l/B<cmp.l/B;
	}
}qr[maxn];
struct node{
	ll pos;int val;
};
int t[maxn],a[maxn];
ll pre[maxn],upre[maxn],dpre[maxn];
vector<ll>mp;
int buc[maxn<<2];
int buc2[maxn<<2];
ll res=0;
ll ans[maxn];
void sol(){
	int l=1,r=0;
	for(int i=1;i<=q;i++){
		while(l>qr[i].l){
			--l;
			++buc[pre[l-1]];
			++buc2[pre[l]];
			res+=buc2[upre[l-1]];
		}
		while(r<qr[i].r){
			++r;
			++buc[pre[r-1]];
			++buc2[pre[r]];
			res+=buc[dpre[r]];
		}
		while(l<qr[i].l){
			res-=buc2[upre[l-1]];
			--buc[pre[l-1]];
			--buc2[pre[l]];
			++l;
		}
		while(r>qr[i].r){
			res-=buc[dpre[r]];
			--buc[pre[r-1]];
			--buc2[pre[r]];
			--r;
		}
		ans[qr[i].id]=res;
	}
}
signed main(){
	n=read(),m=read();
	B=sqrt(n)+1;
	for(int i=1;i<=n;i++)t[i]=read();
	for(int i=1;i<=n;i++)a[i]=read();
	q=read();
	for(int i=1;i<=q;i++)qr[i].l=read(),qr[i].r=read(),qr[i].id=i;
	sort(qr+1,qr+1+q);

	for(int i=1;i<=n;i++){
		if(t[i]==1)pre[i]=pre[i-1]+a[i];
		else pre[i]=pre[i-1]-a[i];
		mp.push_back(pre[i]);
		mp.push_back(upre[i]=pre[i]+m);
		mp.push_back(dpre[i]=pre[i]-m);
	}
	pre[0]=0,upre[0]=m,dpre[0]=-m;
	mp.push_back(pre[0]);
	mp.push_back(upre[0]);
	mp.push_back(dpre[0]);
	sort(mp.begin(),mp.end());
	mp.erase(unique(mp.begin(),mp.end()),mp.end());
	for(int i=0;i<=n;i++)pre[i]=lower_bound(mp.begin(),mp.end(),pre[i])-mp.begin();
	for(int i=0;i<=n;i++)upre[i]=lower_bound(mp.begin(),mp.end(),upre[i])-mp.begin();
	for(int i=0;i<=n;i++)dpre[i]=lower_bound(mp.begin(),mp.end(),dpre[i])-mp.begin();
	sol();
	for(int i=1;i<=q;i++)cout<<ans[i]<<'\n';
	return 0;
}

P4396 [AHOI2013]作业

题意

给定了一个长度为 \(n\) 的数列和若干个询问,每个询问是关于数列的区间表示数列的第 \(l\) 个数到第 \(r\) 个数。

首先你要统计该区间内大于等于 \(a\),小于等于 \(b\) 的数的个数。

其次是所有大于等于 \(a\),小于等于 \(b\) 的,且在该区间中出现过的数值的个数。

\(n,m\le 10^5\)

做法

开桶记录即可。

容易发现,要对桶查询区间和和进行单点修改。因此,考虑使用树状数组维护。

去重之后的次数类似维护即可。具体地,在修改时判断是否变为 \(0\),是否变为 \(1\)

细节见代码。

代码

#include<bits/stdc++.h>
using namespace std;
inline int read(){
	int ans=0;bool op=0;char ch=getchar();
	while(ch<'0'||'9'<ch){if(ch=='-')op=1;ch=getchar();}
	while('0'<=ch&&ch<='9'){ans=(ans<<1)+(ans<<3)+(ch^48);ch=getchar();}
	if(op)return -ans;
	return ans;
}
const int maxn=1e5+10;
int n,m;
int B;
int a[maxn];
pair<int,int> ans[maxn];
vector<int>mp;
struct query{
	int l,r,a,b,id;
	bool operator < (const query cmp)const{
		if(l/B==cmp.l/B)return r<cmp.r;
		return l/B<cmp.l/B;
	}
}q[maxn];
struct TREE{//封装的树状数组
	int tr[maxn];
	TREE(){
		memset(tr,0,sizeof(tr));
	}
	int lowbit(int x){return x&(-x);}
	void add(int pos,int val){
		while(pos<=n){
			tr[pos]+=val;
			pos+=lowbit(pos);
		}
	}
	int query(int pos){
		int ans=0;
		while(pos){
			ans+=tr[pos];
			pos-=lowbit(pos);
		}
		return ans;
	}
}sum,app;
int cnt[maxn];
void update(int pos,int val){//加入或删除元素
	if(cnt[pos]==1&&val==-1)app.add(pos,-1);
	cnt[pos]+=val;
	sum.add(pos,val);
	if(cnt[pos]==1&&val==1)app.add(pos,1);
}
pair<int,int> get(int l,int r){//区间查询
	return make_pair(sum.query(r)-sum.query(l-1),app.query(r)-app.query(l-1));
}
void sol(){//莫队
	int l=1,r=0;
	for(int i=1;i<=m;i++){
		while(r<q[i].r){
			++r;
			update(a[r],1);
		}
		while(l>q[i].l){
			--l;
			update(a[l],1);
		}
		while(r>q[i].r){
			update(a[r],-1);
			--r;
		}
		while(l<q[i].l){
			update(a[l],-1);
			++l;
		}
		ans[q[i].id]=get(q[i].a,q[i].b);
	}
}
signed main(){
	n=read(),m=read();
	B=sqrt(n)+1;
	for(int i=1;i<=n;i++)a[i]=read();
	for(int i=1;i<=m;i++)q[i].l=read(),q[i].r=read(),q[i].a=read(),q[i].b=read(),q[i].id=i;

	for(int i=1;i<=n;i++)mp.push_back(a[i]);
	for(int i=1;i<=m;i++)mp.push_back(q[i].a);
	for(int i=1;i<=m;i++)mp.push_back(q[i].b);
	sort(mp.begin(),mp.end());
	mp.erase(unique(mp.begin(),mp.end()),mp.end());
	for(int i=1;i<=n;i++)a[i]=lower_bound(mp.begin(),mp.end(),a[i])-mp.begin()+1;
	for(int i=1;i<=m;i++)q[i].a=lower_bound(mp.begin(),mp.end(),q[i].a)-mp.begin()+1;
	for(int i=1;i<=m;i++)q[i].b=lower_bound(mp.begin(),mp.end(),q[i].b)-mp.begin()+1;
	//离散化

	sort(q+1,q+1+m);
	sol();//莫队
	for(int i=1;i<=m;i++)cout<<ans[i].first<<' '<<ans[i].second<<'\n';
	return 0;
}

小技巧

莫队的初值可以设为 \(l=1,r=0\),这样就不用特殊算一遍 \(1\) 号位置的贡献了。

结尾

这篇比较短,就写到这里了。后面会写个树上莫队笔记。


\[\Huge\mathcal{The}\space\mathcal{End}. \]

posted @ 2023-06-16 10:51  洛谷Augury  阅读(23)  评论(0编辑  收藏  举报