莫队算法学习笔记
其实莫涛据说是 xqz 的学生,可以算是我的学长。
以下记 \(n\) 为序列 \(a_1,a_2,\cdots,a_n\) 的长度,\(q\) 为询问次数(与修改次数之和)。
对序列以 \(B\) 为块长分块,记 \(\operatorname{pos}(i),\operatorname{edgeL}(i),\operatorname{edgeR}(i)\) 分别为 \(a_i\) 的块编号(即 \(\left\lfloor\dfrac iB\right\rfloor\)),块 \(i\) 的左端点(即 \((i-1)B+1\)),块 \(i\) 的右端点(即 \(iB\))。
普通莫队算法
莫队算法是一种用于处理序列上的区间询问问题的离线算法。
对序列以 \(B\) 为块长分块。
若区间 \([l,r]\) 的答案能够 \(\mathcal O(k)\) 扩展到 \([l-1,r],[l+1,r],[l,r-1],[l,r+1]\),视询问次数 \(q\) 与 \(n\) 同阶,则可以在 \(\mathcal O\left(nk\sqrt n\right)\) 的复杂度内解决。通常情况下,可以取 \(\mathcal O(k)=\mathcal O(1)\),但是有的时候带 log 也是可以接受的。
以下分析时视为 \(\mathcal O(1)\) 扩展区间,记从区间 \([l,r]\) 扩展到 \([l',r]\) 为 \([l,r]\rightarrow[l',r']\)。
而莫队算法的思想也很简单,离线后排序,对于 \([l,r]\) 按照 \(\operatorname{pos}(l)\) 升序、\(r\) 直接升序排序。
设当前维护的区间为 \([l,r]\)。则 \([l_1,r_1]\rightarrow[l_2,r_2]\) 过程中一定要保证 \(l\leq r\),否则答案会错误。(也可以用 \(l=r+1\) 来表示空区间)
具体而言,先将 \([l,r]\) 向左扩展 \(l\),再向右扩展 \(r\),之后才是向右扩展 \(l\) 和向左扩展 \(r\);区间扩展的顺序不能随意修改。
称 \([l,r]\rightarrow[l-1,r]\) 为 addLeft 操作,\([l,r]\rightarrow[l,r+1]\) 为 addRight 操作,\([l,r]\rightarrow[l+1,r]\) 为 delLeft 操作,\([l,r]\rightarrow[l,r-1]\) 为 delRight 操作。同时,在 \([l_1,r_1]\rightarrow[l_2,r_2]\) 过程中 addLeft、delLeft 至多有一个会发生,原因显然;addRight、delRight 同理。[1]
那么操作顺序为:addLeft、 addRight、 delLeft 、delRight。
对于初始状态 \([l_0,r_0]\),可以直接取 \([1,1]\) 并手动计算答案,也可以取 \([2,1]\) 表示空区间。
普通莫队算法模板代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
constexpr const int N=27000,M=27000;
int n,m,a[N+1];
struct question{
int l,r,id;
}q[M+1];
int ans[N+1];
int B,pos[N+1],edgeL[N+1],edgeR[N+1];
void pre(){
B=sqrt(n);
for(int i=1;edgeR[i-1]+1<=n;i++){
edgeL[i]=edgeR[i-1]+1;
edgeR[i]=min(edgeL[i]+B-1,n);
for(int j=edgeL[i];j<=edgeR[i];j++){
pos[j]=i;
}
}
sort(q+1,q+m+1,[](question a,question b){
if(pos[a.l]!=pos[b.l]){
return pos[a.l]<pos[b.l];
}else{
return a.r<b.r;
}
});
}
void addLeft(int &l,int &r,int &cnt){
l--;
//do something.
}
void addRight(int &l,int &r,int &cnt){
r++;
//do something.
}
void delLeft(int &l,int &r,int &cnt){
//do something.
l++;
}
void delRight(int &l,int &r,int &cnt){
//do something.
r--;
}
int main(){
/*freopen("test.in","r",stdin);
freopen("test.out","w",stdout);*/
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m;
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;
}
pre();
//cnt:记录答案
int l=2,r=1,cnt=0;
for(int i=1;i<=m;i++){
while(q[i].l<l){
addLeft(l,r,cnt);
}
while(r<q[i].r){
addRight(l,r,cnt);
}
while(l<q[i].l){
delLeft(l,r,cnt);
}
while(q[i].r<r){
delRight(l,r,cnt);
}
//do something.
ans[q[i].id]=cnt;
}
for(int i=1;i<=m;i++){
cout<<ans[i]<<'\n';
}
cout.flush();
/*fclose(stdin);
fclose(stdout);*/
return 0;
}
奇偶化排序
常数优化方案。考虑 $r$ 扫到最右边后,$l$ 的块编号更新时,$r$ 又要扫到最左边,再扫回最右边。
扫回左边的过程中,其实已经可以得到一些区间的答案,但是这些答案在之后第二次扫到右边的时候才被统计。因此可以对于奇数块编号 $l$,$r$ 按照升序排序,偶数块编号 $l$,$r$ 按照降序排序。
据说常数可以提升约 $30\%$,但是我觉得没什么用。
关于复杂度。
可以发现,\([l_1,r_1]\rightarrow[l_2,r_2]\) 的复杂度是 \(\mathcal O(l_2-l_1+r_2-r_1)\)。
- 考虑 \(l_1,l_2\) 按照块编号排序,有单次移动代价 \(\mathcal O(l_2-l_1)=\mathcal O(B)\),总代价为 \(\mathcal O(qB)\)。(当 \(l_1,l_2\) 的块编号相隔过大时,这一部分的总复杂度为 \(\mathcal O(n)\),可忽略)
- 考虑 \(r_1,r_2\) 从小到大排序,因此当当前区间 \(l\) 块编号不变时,移动 \(r\) 的总代价为 \(\mathcal O(n)\),\(l\) 的块编号至多移动 \(\mathcal O\left(\dfrac nB\right)\) 次,总代价 \(\mathcal O\left(\dfrac{n^2}B\right)\)。
因此总时间复杂度为 \(\mathcal O\left(qB+\dfrac{n^2}B\right)\)。取 \(B=\dfrac{n}{\sqrt q}\) 可得最优复杂度 \(\mathcal O\left(n\sqrt q\right)\)。视 \(n,q\) 同阶,即 \(\mathcal O\left(n\sqrt n\right)\)。
同时,\(B\) 的取值会影响莫队算法的复杂度,例如当 \(q=\sqrt n\) 时,取 \(B=n^{\frac 34}\) 可得最优复杂度 \(\mathcal O\left(n^{\frac 54}\right)\),而直接取 \(B=\sqrt n\) 不优。
给定值域 \([1,k]\) 的序列 \(a_1,a_2,\cdots,a_n\),有 \(m\) 个区间 \([l_1,r_1],\cdots,[l_m,r_m]\),对于每一个区间 \([l_i,r_i]\),求:
\[\sum_{j=1}^kc_j^2 \]\(c_j\) 表示 \(a_{l_i},a_{l_i+1},\cdots,a_{r_i}\) 中 \(j\) 的出现次数,\(1\leq n,m,k\leq5\times10^4\)。
可离线区间询问,考虑莫队。
区间扩展是简单的,维护桶 \(c\) 即可。
参考代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
typedef long long ll;
constexpr const int N=5e4,M=5e4,V=5e4;
int n,m,k,a[N+1],c[V+1];
struct question{
int l,r,id;
}q[M+1];
int ans[N+1];
int B,pos[N+1],edgeL[N+1],edgeR[N+1];
void pre(){
B=sqrt(n);
for(int i=1;edgeR[i-1]+1<=n;i++){
edgeL[i]=edgeR[i-1]+1;
edgeR[i]=min(edgeL[i]+B-1,n);
for(int j=edgeL[i];j<=edgeR[i];j++){
pos[j]=i;
}
}
sort(q+1,q+m+1,[](question a,question b){
if(pos[a.l]!=pos[b.l]){
return pos[a.l]<pos[b.l];
}else{
return a.r<b.r;
}
});
}
void addLeft(int &l,int &r,ll &cnt){
l--;
cnt-=1ll*c[a[l]]*c[a[l]];
c[a[l]]++;
cnt+=1ll*c[a[l]]*c[a[l]];
}
void addRight(int &l,int &r,ll &cnt){
r++;
cnt-=1ll*c[a[r]]*c[a[r]];
c[a[r]]++;
cnt+=1ll*c[a[r]]*c[a[r]];
}
void delLeft(int &l,int &r,ll &cnt){
cnt-=1ll*c[a[l]]*c[a[l]];
c[a[l]]--;
cnt+=1ll*c[a[l]]*c[a[l]];
l++;
}
void delRight(int &l,int &r,ll &cnt){
cnt-=1ll*c[a[r]]*c[a[r]];
c[a[r]]--;
cnt+=1ll*c[a[r]]*c[a[r]];
r--;
}
int main(){
/*freopen("test.in","r",stdin);
freopen("test.out","w",stdout);*/
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m>>k;
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;
}
pre();
//cnt:记录答案
int l=2,r=1;
ll cnt=0;
for(int i=1;i<=m;i++){
while(q[i].l<l){
addLeft(l,r,cnt);
}
while(r<q[i].r){
addRight(l,r,cnt);
}
while(l<q[i].l){
delLeft(l,r,cnt);
}
while(q[i].r<r){
delRight(l,r,cnt);
}
ans[q[i].id]=cnt;
}
for(int i=1;i<=m;i++){
cout<<ans[i]<<'\n';
}
cout.flush();
/*fclose(stdin);
fclose(stdout);*/
return 0;
}
带修莫队
如果可以 \(\mathcal O(1)\) 扩展区间,\(\mathcal O(1)\) 得到修改操作对于区间答案的影响,则可以在 \(\mathcal O\left(n^{\frac53}\right)\) 的复杂度内完成。
考虑当序列存在修改操作时,普通莫队便无法处理了——因为区间离线后,无法判断当前区间内的序列到底是什么。
考虑升维,将区间 \([l,r]\) 扩展到 \([l,r,t]\),\(t\) 为时间维。那么原本需要离线的 \([l_i,r_i,t_i]\) 的 \(t_i\) 便表示这是第 \(t_i\) 次修改操作之后的询问区间。
对序列 \(a\) 以块长 \(B\) 分块。
将区间 \([l_i,r_i,t_i]\) 排序时,先后按照 \(\operatorname{pos}(l_i),\operatorname{pos}(r_i),t_i\) 升序排序即可。
同样存在普通莫队中的 addLeft、addRight、delLeft、delRight,用于在 \(t\) 一定时「横向」扩展区间。
当 \([l_1,r_1,t_1]\rightarrow[l_2,r_2,t_1]\) 之后,便可以通过 moveUp、moveDown 操作来「纵向」扩展区间,从而使 \([l_2,r_2,t_2]\rightarrow[l_2,r_2,t_2]\)。称 \([l,r,t]\rightarrow[l,r,t+1]\) 为 moveUp 操作,\([l,r,t]\rightarrow[l,r,t-1]\) 为 moveDown 操作,这两种操作过程中只需要注意修改操作对于区间 \([l,r]\) 的答案的贡献即可。
关于复杂度。
考虑 \([l_1,r_1,t_1]\rightarrow[l_2,r_2,t_2]\)。
- \([l_1,r_1]\rightarrow[l_2,r_2]\) 是 \(\mathcal O(B)\) 的,总复杂度 \(\mathcal O(qB)\)。
- 考虑 \(l,r\) 所在块编号不变时,\(t_1\rightarrow t_2\) 的总复杂度是 \(\mathcal O(q)\) 的。那么 \(t_1\rightarrow t_2\) 的总复杂度便为 \(\mathcal O\left(\dfrac{qn^2}{B^2}\right)\)。
总复杂度便为 \(\mathcal O\left(qB+\dfrac{qn^2}{B^2}\right)\),取 \(B=\sqrt[3]{n^2}=n^{\frac 23}\) 最优,\(\mathcal O\left(qn^{\frac 23}\right)\)。视 \(n,q\) 同阶,则有最优复杂度 \(\mathcal O\left(n^{\frac 53}\right)\)。
luogu P1903 [国家集训队] 数颜色 / 维护队列 /【模板】带修莫队
给定序列 \(a_1,a_2,\cdots,a_n\),\(m\) 次操作:
- 询问 \(a_l,a_{l+1},\cdots,a_r\) 内不同的数的个数。
- 将 \(a_i\) 修改为 \(c\)。
\(1\leq n,m\leq133333,1\leq a_i,c\leq10^6\)。
考虑带修莫队,维护一个桶记录是否出现在区间内,以及维护好时间维上的偏移即可。
参考代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
constexpr const int N=133333,M=133333,V=1e6;
int n,m,sizeQ,sizeOp,a[N+1];
struct question{
int l,r,t,id;
}q[M+1];
struct operation{
int pos,color,backup;
}op[M+1];
int ans[M+1];
int B,size,pos[N+1],edgeL[N+1],edgeR[N+1];
void pre(){
B=pow(n,2/3.0);
for(int i=1;edgeR[i-1]+1<=n;i++){
edgeL[i]=edgeR[i-1]+1;
edgeR[i]=min(edgeL[i]+B-1,n);
for(int j=edgeL[i];j<=edgeR[i];j++){
pos[j]=i;
}
}
sort(q+1,q+sizeQ+1,[](question a,question b){
if(pos[a.l]!=pos[b.l]){
return pos[a.l]<pos[b.l];
}else if(pos[a.r]!=pos[b.r]){
return pos[a.r]<pos[b.r];
}else{
return a.t<b.t;
}
});
}
void addLeft(int &l,int &r,int &t,int cnt[],int &pl){
l--;
cnt[a[l]]++;
if(cnt[a[l]]==1){
pl++;
}
}
void addRight(int &l,int &r,int &t,int cnt[],int &pl){
r++;
cnt[a[r]]++;
if(cnt[a[r]]==1){
pl++;
}
}
void delLeft(int &l,int &r,int &t,int cnt[],int &pl){
cnt[a[l]]--;
if(!cnt[a[l]]){
pl--;
}
l++;
}
void delRight(int &l,int &r,int &t,int cnt[],int &pl){
cnt[a[r]]--;
if(!cnt[a[r]]){
pl--;
}
r--;
}
void moveUp(int &l,int &r,int &t,int cnt[],int &pl){
t++;
if(l<=op[t].pos&&op[t].pos<=r){
cnt[a[op[t].pos]]--;
if(!cnt[a[op[t].pos]]){
pl--;
}
}
a[op[t].pos]=op[t].color;
if(l<=op[t].pos&&op[t].pos<=r){
cnt[a[op[t].pos]]++;
if(cnt[a[op[t].pos]]==1){
pl++;
}
}
}
void moveDown(int &l,int &r,int &t,int cnt[],int &pl){
if(l<=op[t].pos&&op[t].pos<=r){
cnt[a[op[t].pos]]--;
if(!cnt[a[op[t].pos]]){
pl--;
}
}
a[op[t].pos]=op[t].backup;
if(l<=op[t].pos&&op[t].pos<=r){
cnt[a[op[t].pos]]++;
if(cnt[a[op[t].pos]]==1){
pl++;
}
}
t--;
}
int main(){
/*freopen("test.in","r",stdin);
freopen("test.out","w",stdout);*/
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m;
static int real[N+1];
for(int i=1;i<=n;i++){
cin>>a[i];
real[i]=a[i];
}
while(m--){
char ch;
cin>>ch;
switch(ch){
case 'Q':
sizeQ++;
cin>>q[sizeQ].l>>q[sizeQ].r;
q[sizeQ].t=sizeOp;
q[sizeQ].id=sizeQ;
break;
case 'R':
sizeOp++;
cin>>op[sizeOp].pos>>op[sizeOp].color;
op[sizeOp].backup=real[op[sizeOp].pos];
real[op[sizeOp].pos]=op[sizeOp].color;
break;
}
}
pre();
int l=1,r=1,t=0,pl=1;
static int cnt[V+1];
cnt[a[1]]=1;
for(int i=1;i<=sizeQ;i++){
while(q[i].l<l){
addLeft(l,r,t,cnt,pl);
}
while(r<q[i].r){
addRight(l,r,t,cnt,pl);
}
while(l<q[i].l){
delLeft(l,r,t,cnt,pl);
}
while(q[i].r<r){
delRight(l,r,t,cnt,pl);
}
while(t<q[i].t){
moveUp(l,r,t,cnt,pl);
}
while(q[i].t<t){
moveDown(l,r,t,cnt,pl);
}
ans[q[i].id]=pl;
}
for(int i=1;i<=sizeQ;i++){
cout<<ans[i]<<'\n';
}
cout.flush();
/*fclose(stdin);
fclose(stdout);*/
return 0;
}
回滚莫队
有一个长度为 \(n\) 的序列 \(a_1,a_2,\cdots,a_n\) 和 \(m\) 次询问,第 \(i\) 次询问区间 \([l_i,r_i]\) 内最小的未出现的自然数。(即 \(\operatorname{mex}\))
\(1\leq n,m,a_i\leq2\times10^5\)。
如果使用普通莫队,则需要维护 \(\operatorname{mex}\) 的增加/删除。增加、删除数都很好用桶维护,但是无法快速得出答案。
如果使用堆等数据结构存储下来,则会带上一个 log,会被卡。想要 \(\mathcal O(1)\) 得到答案,只能开桶记录是否出现,区间删除时可以直接更新 \(\operatorname{mex}\),但是区间增加时想要更新 \(\operatorname{mex}\),就只能暴力跳,复杂度会假。
普通莫队配合
bitset维护普通莫队配合
bitset其实是可以维护 $\operatorname{mex}$ 的。bitset有一个成员函数_Find_first(),可以在 $\mathcal O\left(\dfrac{\textit{size}}{w}\right)$ 的复杂度内查询第一个 $1$ 的下标。这样可以做到 $\mathcal O\left(n\sqrt n+\dfrac{nq}w\right)$ 维护,跑不满。
放一份普通莫队配合
bitset、奇偶化排序的 AC 代码。参考代码
//#include<bits/stdc++.h> #include<algorithm> #include<iostream> #include<cstring> #include<iomanip> #include<cstdio> #include<string> #include<vector> #include<cmath> #include<ctime> #include<deque> #include<queue> #include<stack> #include<list> #include<bitset> using namespace std; constexpr const int N=2e5,M=2e5,V=2e5; int n,m,a[N+1]; struct question{ int l,r,id; }q[M+1]; int ans[M+1]; int B,size,pos[N+1],edgeL[N+1],edgeR[N+1]; void pre(){ B=sqrt(n); for(int i=1;edgeR[i-1]+1<=n;i++){ edgeL[i]=edgeR[i-1]+1; edgeR[i]=min(edgeL[i]+B-1,n); for(int j=edgeL[i];j<=edgeR[i];j++){ pos[j]=i; } } sort(q+1,q+m+1,[](question a,question b){ if(pos[a.l]!=pos[b.l]){ return pos[a.l]<pos[b.l]; }else{ if(pos[a.l]&1){ return a.r<b.r; }else{ return b.r<a.r; } } }); } void addLeft(int &l,int &r,int cnt[],bitset<V+1+1> &mex){ l--; cnt[a[l]]++; mex[a[l]]=0; } void addRight(int &l,int &r,int cnt[],bitset<V+1+1> &mex){ r++; cnt[a[r]]++; mex[a[r]]=0; } void delLeft(int &l,int &r,int cnt[],bitset<V+1+1> &mex){ cnt[a[l]]--; if(!cnt[a[l]]){ mex[a[l]]=1; } l++; } void delRight(int &l,int &r,int cnt[],bitset<V+1+1> &mex){ cnt[a[r]]--; if(!cnt[a[r]]){ mex[a[r]]=1; } r--; } int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ cin>>n>>m; 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; } pre(); int l=1,r=1; static int cnt[V+1+1]; cnt[a[1]]++; bitset<V+1+1>mex; mex.set(); mex[a[1]]=0; for(int i=1;i<=m;i++){ while(q[i].l<l){ addLeft(l,r,cnt,mex); } while(r<q[i].r){ addRight(l,r,cnt,mex); } while(l<q[i].l){ delLeft(l,r,cnt,mex); } while(q[i].r<r){ delRight(l,r,cnt,mex); } ans[q[i].id]=mex._Find_first(); } for(int i=1;i<=m;i++){ cout<<ans[i]<<'\n'; } /*fclose(stdin); fclose(stdout);*/ return 0; }
莫队算法的区间扩展分两种,增加(addLeft、addRight)和删除(delLeft、delRight)。
当增加、删除只有一种操作能够较为高效地(\(\mathcal O(1)\))维护时,便可以使用回滚莫队来解决。
回滚莫队的思想很简单——既然只能用一种操作,那就只用一种操作。废话。
具体而言,回滚莫队分为两种:只加不减的回滚莫队[2]和只减不加的回滚莫队[3]。
不增加莫队
对于询问区间,按照左端点块编号升序排序,右端点降序排序。
维护区间 \([l,r]\),当 \(l\) 块编号一定时,\(r\) 显然只需要从右往左不断删点 delRight 即可。但是此时 \(l\) 的顺序是乱序的,可能还是需要 addLeft 操作和 delLeft 操作。
记 \(\textit{ans}\) 为所维护区间 \([l,r]\) 的答案。
考虑确定块编号,初始化区间 \([l,r]=[\operatorname{edgeL}(\operatorname{pos}(l)),n]\),并记录 \(\textit{ans}=\textit{ans}_{\text n}\)。
那么维护 \([l,r]\rightarrow[l_1,r_1]\) 时,\(r\rightarrow r_1\) 是很好先通过 delRight 操作维护的。\([l,r]\rightarrow[l,r_1]\) 后,记录 \(\textit{ans}_0=\textit{ans}\)。
此时对于左端点,每次都不断 delLeft 使得 \(l\rightarrow l_1\),最终使得 \([l,r]\rightarrow[l_1,r_1]\),得到答案 \(\textit{ans}\)。记录下来答案后,将状态 \([l,r]\) 直接回滚到 \([\operatorname{edgeL}(\operatorname{pos}(l)),r_1]\),\(\textit{ans}\) 回滚到 \(\textit{ans}_0\) 即可。
同时 \(\operatorname{edgeL}(\operatorname{pos}(l))\) 变化时,也可以回滚到状态 \([l,r]=[\operatorname{edgeL}(\operatorname{pos}(l)),n]\),\(\textit{ans}=\textit{ans}_{\text n}\),再不断 delLeft \([l,n]\rightarrow[\operatorname{edgeL}(\operatorname{pos}(l)+1),n]\) 即可。
关于复杂度。
甚至于从某种意义上来说,回滚莫队比普通莫队更为简单。
显然对于 \(q\) 次询问,每一次询问移动 \(l\) 都是 \(\mathcal O(B)\),总复杂度为 \(\mathcal O(qB)\)。块编号改变时还有 \(\mathcal O(n)\) 的总复杂度。
考虑移动 \(r\),单个块内的总复杂度为 \(\mathcal O(n)\),则所有块的总复杂度为 \(\mathcal O\left(\dfrac {n^2}B\right)\)。
故总复杂度为 \(\mathcal O\left(qB+\dfrac{n^2}B\right)\),取 \(B=\dfrac{n}{\sqrt q}\) 可得最优复杂度 \(\mathcal O\left(n\sqrt q\right)\)。视 \(n,q\) 同阶,即 \(\mathcal O\left(n\sqrt n\right)\)。
不删除莫队
类似地,反过来即可。

浙公网安备 33010602011771号