笔记·普通莫队
笔记·莫队
形式
假设 \(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\) 同阶)。对左右端点分别考虑:
- 左端点会移动 \(O(q\sqrt{n})\) 次。因为我们按照 \(l\) 所在的块进行了排序,所以对于每个询问,\(l\) 会移动 \(\sqrt{n}\) 次。
- 右端点会移动 \(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\) 号位置的贡献了。
结尾
这篇比较短,就写到这里了。后面会写个树上莫队笔记。