莫队
参考资料:
关于莫队:
一种利用 分块 特点将 区间操作 进行 离线处理 的 暴力。(暴论)
莫队算法是由前国家队队长莫涛提出的算法。最开始的莫队是将区间询问按照 特定顺序 排序后,通过逐步移动左右端点求出各询问答案。后来又延伸出了“树上莫队”、“带修莫队”、“树上带修莫队”、“回滚莫队”……
静态莫队:
例题:Luogu P3901
(其实这题有一万种写法,甚至有的写法更优,但作为莫队的练手题还是不错的)
关于最普通的暴力算法,则是将每个询问的区间都遍历一边,时间复杂度 \(O(nm)\)
而莫队作为一种高级暴力,显然不能这么low。
在莫队算法中,首先将所有询问按照左端点分块,每个块大小为 \(\sqrt n\) 。再在同一个块内按照右端点从小到大排序。以特定顺序排序后,在建议一个“当前查询区间”,其左右端点移动时答案跟着改变,当“当前查询区间”与询问区间刚好吻合是,当前答案便是询问答案。
按照以上顺序排序后,当左端点在块内移动,每次移动距离不超过 \(\sqrt n\) ,时间复杂度最大为 \(O(m \sqrt n)\) 。当左端点在块与块之间移动,因为从上一个块移动到下一个块只会发生一次,且不会返回,因此时间复杂度为 \(O(n)\) 。
而对于右端点,当左端点在同一个块内时,右端点只会向右移动,时间复杂度 \(O(n \sqrt n)\) 。当左端点在块与块之间移动时,右端点只会从右往左移动一次 因此时间复杂度又是 \(O(n \sqrt n)\)。
综上,莫队算法的时间复杂度大致是 \(O((n+m)\sqrt n)\) ,明显比纯暴力优秀了许多。
实现如下:
#include<bits/stdc++.h>
#define reg register
#define lb lower_bound
#define rep(i,a,b) for(int i=a,i##end=b;i<=i##end;++i)
#define drep(i,a,b) for(int i=a,i##end=b;i>=i##end;--i)
using namespace std;
bool Handsome;
inline int rd(){
reg int x=0;reg char o=getchar();reg bool O=0;
for(;o<48 || 57<o;o=getchar())if(o=='-')O=1;
for(;48<=o && o<=57;o=getchar())x=(x<<1)+(x<<3)+(o^48);
return O?-x:x;
}
const int M=2e5+5;
int n,sn,m,a[M],c[M],t[M],bl[M],l,r,ans[M],ANS;
struct node{int x,y,id;}A[M];
bool cmp(node a,node b){
if(bl[a.x]==bl[b.x])return a.y<b.y;
return a.x<b.x;
}
void add(int x){
if(t[a[x]]==1)++ANS;
++t[a[x]];
}
void del(int x){
--t[a[x]];
if(t[a[x]]==1)--ANS;
}
bool Most;
int main(){
// printf("%.2lfMB\n",(&Most-&Handsome)/1024.0/1024.0);
n=rd(),m=rd();sn=sqrt(n);
rep(i,1,n)bl[i]=(i-1)/sn+1;
rep(i,1,n)c[i]=a[i]=rd();
sort(c+1,c+n+1);
rep(i,1,n)a[i]=lb(c+1,c+n+1,a[i])-c;
rep(i,1,m)A[A[i].id=i].x=rd(),A[i].y=rd();
sort(A+1,A+m+1,cmp);l=1;
rep(i,1,m){
while(r<A[i].y)add(++r);
while(l>A[i].x)add(--l);
while(r>A[i].y)del(r--);
while(l<A[i].x)del(l++);
ans[A[i].id]=ANS;
}
rep(i,1,m)puts(ans[i]?"No":"Yes");
return 0;
}
如果想要再快一些,还可以使用奇偶优化:
bool cmp(node a,node b){
if(bl[a.x]==bl[b.x])return bl[a.x]&1?a.y<b.y:a.y>b.y;
return a.x<b.x;
}
树上莫队:
关于子树的区间查询还是非常好想的,这里不作细述,主要讲讲路径查询。
都知道莫队是区间查询,想要让莫队实现路径查询,就要把路径变成区间。
这里需要使用一种特殊的dfs序:欧拉序
关于以下这棵树(图的来源)

若询问路径 4-6 ,如果取第二个 4 到第一个 6 ,则为:4 5 5 2 3 6 。
其中不属于该路径的 5 出现了两次,以及作为 lca 的 1 没有出现,而其他的数都有且只有一个。
则此类路径询问方式可以是:询问欧拉序较小的 secend 到欧拉序较大的 first ,其中出现了两次的数删去,最后补上 lca 。
若询问路径 3-10 ,如果取第一个 3 和第一个 10 ,则为:3 6 8 9 9 10 。
同样是不属于路径的 9 出现了两次,且所有数都有且只有一个。
则此类路径询问方式可以是:询问欧拉序较小的 first 到欧拉序较大的 first ,其中出现了两次的数删去。
综上,关于树上莫队,只需注意:
-
处理欧拉序。
-
在询问时判断一下路径类型,对应相应的区间。
-
查询是注意判断每个数的出现次数。
-
最后注意是否需要补上 lca 。
实现如下:
#include<bits/stdc++.h>
#define reg register
#define lb lower_bound
#define rep(i,a,b) for(int i=a,i##end=b;i<=i##end;++i)
#define drep(i,a,b) for(int i=a,i##end=b;i>=i##end;--i)
#define erep(i,a) for(int i=head[a];i;i=e[i].nxt)
using namespace std;
bool Handsome;
inline int rd(){
reg int x=0;reg char o=getchar();
while(o<48)o=getchar();
while(48<=o)x=(x<<1)+(x<<3)+(o^48),o=getchar();
return x;
}
const int M=1e5+5,B=16;
int n,m,sn,a[M],bl[M],head[M],cnte,fir[M],sec[M],b[M<<1],bt;
int fa[M][B+2],dep[M],l,r,ANS,ans[M],mk[M],c[M];
struct edge{int to,nxt;}e[M<<1];
void Add(int x,int y){
e[++cnte]=(edge){y,head[x]};
head[x]=cnte;
}
struct node{int x,y,z,id;}A[M];
bool cmp(node a,node b){
if(bl[a.x]==bl[b.x])return bl[a.x]&1?a.y<b.y:a.y>b.y;
return a.x<b.x;
}
void dfs(int x,int Fa){
b[fir[x]=++bt]=x;
dep[x]=dep[fa[x][0]=Fa]+1;
rep(j,1,B)fa[x][j]=fa[fa[x][j-1]][j-1];
erep(i,x){
int y=e[i].to;
if(y==Fa)continue;
dfs(y,x);
}
b[sec[x]=++bt]=x;
}
int LCA(int x,int y){
if(dep[x]<dep[y])swap(x,y);
drep(j,B,0)if(dep[fa[x][j]]>=dep[y])x=fa[x][j];
if(x==y)return x;
drep(j,B,0)if(fa[x][j]!=fa[y][j])
x=fa[x][j],y=fa[y][j];
return fa[x][0];
}
int t[M];
void add(int x){
if(!t[x])++ANS;
++t[x];
}
void del(int x){
--t[x];
if(!t[x])--ANS;
}
bool Most;
int main(){
// printf("%.2lfMB\n",(&Most-&Handsome)/1024.0/1024.0);
n=rd();m=rd();sn=sqrt(n);
rep(i,1,n<<1)bl[i]=(i-1)/sn+1;
rep(i,1,n)c[i]=a[i]=rd();
sort(c+1,c+n+1);
rep(i,1,n)a[i]=lb(c+1,c+n+1,a[i])-c;
rep(i,2,n){
int x=rd(),y=rd();
Add(x,y);Add(y,x);
}
dfs(1,0);
rep(i,1,m){
int x=rd(),y=rd();
if(fir[x]>fir[y])swap(x,y);
int z=LCA(x,y);
if(z==x)A[i]=(node){fir[x],fir[y],0,i};
else A[i]=(node){sec[x],fir[y],z,i};
}
sort(A+1,A+m+1,cmp);l=1;
rep(i,1,m){
while(r<A[i].y){
mk[b[++r]]^=1;
if(mk[b[r]])add(a[b[r]]);
if(!mk[b[r]])del(a[b[r]]);
}
while(l>A[i].x){
mk[b[--l]]^=1;
if(mk[b[l]])add(a[b[l]]);
if(!mk[b[l]])del(a[b[l]]);
}
while(r>A[i].y){
mk[b[r]]^=1;
if(mk[b[r]])add(a[b[r]]);
if(!mk[b[r]])del(a[b[r]]);
--r;
}
while(l<A[i].x){
mk[b[l]]^=1;
if(mk[b[l]])add(a[b[l]]);
if(!mk[b[l]])del(a[b[l]]);
++l;
}
if(A[i].z)add(a[A[i].z]);
ans[A[i].id]=ANS;
if(A[i].z)del(a[A[i].z]);
}
rep(i,1,m)printf("%d\n",ans[i]);
return 0;
}
带修莫队:
例题:Luogu P1903
带修莫队实际上就是在左右端点的基础上又加了个时间轴,其中时间轴用来修改各点的值。
只需将左、右端点分块,再将时间轴排序即可,本质上与普通莫队差别不大。
要注意的是块的大小要取 \(n^{\tfrac{2}{3}}\) ,时间复杂度为 \(O(n^{\tfrac{5}{3}})\) ,如果取 \(\sqrt n\) 会退化成 \(O(n^2)\) 。
直接上代码吧……
#include<bits/stdc++.h>
#define reg register
#define rep(i,a,b) for(int i=a,i##end=b;i<=i##end;++i)
#define erep(i,a) for(int i=head[a];i;i=e[i].nxt)
using namespace std;
bool Handsome;
const int M=1e6+5;
int n,m,p,q,sn,a[M],bl[M],t[M],l,r,ans[M],ANS;
char s[5];
struct node{int x,y,z,id;}A[M],B[M];
bool cmp(node a,node b){
if(bl[a.x]==bl[b.x]){
if(bl[a.y]==bl[b.y])return bl[a.y]&1?a.z>b.z:a.z<b.z;
return bl[a.x]?a.y<b.y:a.y>b.y;
}
return a.x<b.x;
}
void add(int x){
if(!t[x])++ANS;
++t[x];
}
void del(int x){
--t[x];
if(!t[x])--ANS;
}
bool Most;
int main(){
// printf("%.2lfMB\n",(&Most-&Handsome)/1024.0/1024.0);
scanf("%d%d",&n,&m);sn=pow(n,2.0/3);
rep(i,1,n)bl[i]=(i-1)/sn+1;
rep(i,1,n)scanf("%d",&a[i]);
rep(i,1,m){
int x,y;scanf("%s%d%d",s,&x,&y);
if(s[0]=='Q')++q,A[q]=(node){x,y,p,q};
if(s[0]=='R')++p,B[p]=(node){x,a[x],y,p},a[x]=y;
}
sort(A+1,A+q+1,cmp);l=1;
rep(i,1,q){
while(r<A[i].y)add(a[++r]);
while(l>A[i].x)add(a[--l]);
while(r>A[i].y)del(a[r--]);
while(l<A[i].x)del(a[l++]);
while(p>A[i].z){
if(l<=B[p].x && B[p].x<=r)del(a[B[p].x]);
a[B[p].x]=B[p].y;
if(l<=B[p].x && B[p].x<=r)add(a[B[p].x]);
--p;
}
while(p<A[i].z){
++p;
if(l<=B[p].x && B[p].x<=r)del(a[B[p].x]);
a[B[p].x]=B[p].z;
if(l<=B[p].x && B[p].x<=r)add(a[B[p].x]);
}
ans[A[i].id]=ANS;
}
rep(i,1,q)printf("%d\n",ans[i]);
return 0;
}
回滚莫队:
例题:Luogu P5906
针对一些如求最值的,只能插入不能删除的操作,求需要用到回滚莫队。
关于回滚莫队,依然是左端点分块,右端点从小到大排序(但似乎不能用奇偶优化了)。但查询时要每个块逐步操作。
对于一个块,先清空,再将 \(r\) 置于块的最右点。
对于一个询问,如果其右端点在块内,则暴力处理,时间复杂度 \(O(m \sqrt n)\) 。如果其右端点在块外,因为排过序,只需右端点往右加,再块内部分暴力即可,时间复杂度 \(O(n \sqrt n)\)。
关键在于暴力部分,操作前要先保存当前状态,操作后再还原,做到“回滚”。
时间复杂度 \(O((n+m)\sqrt n)\)
#include<bits/stdc++.h>
#define reg register
#define pb push_back
#define lb lower_bound
#define rep(i,a,b) for(int i=a,i##end=b;i<=i##end;++i)
using namespace std;
bool Handsome;
inline void Mi(int &x,int y){if(x>y && (x=y));}
inline void Mx(int &x,int y){if(x<y && (x=y));}
const int M=2e5+5;
int n,m,sn,a[M],c[M],bl[M],cnt,L[M],R[M];
struct node{int x,y,id;}tmp;
vector<node> g[M];
bool cmp(node a,node b){return a.y<b.y;}
int mi[M],mx[M],ans[M],ANS,pi[M],px[M];
void add(int x){
Mi(mi[a[x]],x);
Mx(mx[a[x]],x);
Mx(ANS,mx[a[x]]-mi[a[x]]);
}
int solve(int l,int r){
int res=ANS;
rep(i,l,r){
Mi(pi[a[i]],min(mi[a[i]],i));
Mx(px[a[i]],max(mx[a[i]],i));
Mx(res,px[a[i]]-pi[a[i]]);
}
rep(i,l,r)px[a[i]]=0,pi[a[i]]=1e9;
return res;
}
bool Most;
int main(){
// printf("%.2lfMB\n",(&Most-&Handsome)/1024.0/1024.0);
scanf("%d",&n);sn=sqrt(n);
rep(i,1,n)bl[i]=(i-1)/sn+1;cnt=(n-1)/sn+1;
rep(i,1,cnt)L[i]=(i-1)*sn+1,R[i]=i*sn;R[cnt]=n;
rep(i,1,n)scanf("%d",&a[i]),c[i]=a[i];
sort(c+1,c+n+1);
rep(i,1,n)a[i]=lb(c+1,c+n+1,a[i])-c;
scanf("%d",&m);
rep(i,1,m){
int x,y;scanf("%d%d",&x,&y);
g[bl[x]].pb((node){x,y,i});
}
rep(i,1,n)pi[i]=mi[i]=1e9;
rep(i,1,cnt){
sort(g[i].begin(),g[i].end(),cmp);
int r=R[i];
rep(j,0,g[i].size()-1){
tmp=g[i][j];
if(tmp.y<=R[i])ans[tmp.id]=solve(tmp.x,tmp.y);
else{
while(r<tmp.y)add(++r);
ans[tmp.id]=solve(tmp.x,R[i]);
}
}
while(r>R[i])mx[a[r]]=0,mi[a[r]]=1e9,--r;
ANS=0;
}
rep(i,1,m)printf("%d\n",ans[i]);
return 0;
}
二次离线莫队:
例题:Luogu P4887
题目要求 \(a_i \oplus a_j\) 的结果有 \(k\) 个 \(1\) ,设“ \(k\) 个 \(1\) ”为 \(x\) ,则等价为 \(a_i \oplus x = a_j\) 。因此可以将所有“ \(k\) 个 \(1\) ”记下来,共 \(C_{14}^k\) 个,记 \(sum[i][j]\) 为前 \(i\) 个数中(经前一式子)计算结果为 \(j\) 的个数,计算过程中通过作差即可修改答案。
于是内存 6 个 G ,您炸了……
莫队本身就是一次离线操作,但当面对此时无法满足在进行莫队算法的过程中直接得到答案的情况,可以尝试再次离线,将 \(sum[i][j]\) 改为 \(sum[j]\) 进行运算,即二次离线。
莫队一共有4种变化情况:
-
\((l,r)->(l,r+1)\)
-
\((l,r)->(l,r-1)\)
-
\((l,r)->(l-1,r)\)
-
\((l,r)->(l+1,r)\)
设 \(f(i,j)\) 表示 \([1,i]\) 对 \(j\) 的贡献。则针对第 1 种,每次变化时的增加贡献有 \(f(r,r+1)-f(l-1,r+1)\) 。
则 \((l,r_1) -> (l,r_2)\) 增加的贡献就是 \(\sum_{i=r_1+1}^{r_2} f(i-1,i) + \sum_{i=r_1+1}^{r_2} f(l-1,i)\) 。
其中的 \(\sum_{i=r_1+1}^{r_2} f(i-1,i) = \sum_{i=1}^{r_2} f(i-1,i) - \sum_{i=1}^{r_1} f(i-1,i)\) 可以 \(O(n)\) 求 。 \(\sum_{i=r_1+1}^{r_2} f(l-1,i)\) 可以离线求,同时因为第一次离线时使得 \(l,r\) 移动的总距离为 \(O((n+m)\sqrt n)\) 所以这一部分也是 \(O(n\sqrt n)\) 。
其他几种可以用类似的方法推得,这里不细讲了,直接上结果:
令 \(F1(x)=\sum_{i=1}^{x} f(i-1,i)\) , \(F2(x)=\sum_{i=1}^{x} f(i,i)\),则:
-
\((l,r_1)->(l,r_2) (r_1 < r_2)\): \(ans+=F1(r_2)-F1(r_1)-\sum_{i=r_1+1}^{r_2} f(l-1,i)\)
-
\((l,r_1)->(l,r_2) (r_1 > r_2)\): \(ans+=F1(r_2)-F1(r_1)+\sum_{i=r_2+1}^{r_1} f(l-1,i)\)
-
\((l_1,r)->(l_2,r) (l_1 < l_2)\): \(ans+=F2(l_2-1)-F2(l_1-1)-\sum_{i=l_1}^{l_2-1} f(i,r)\)
-
\((l_1,r)->(l_2,r) (l_1 > l_2)\): \(ans+=F2(l_2-1)-F2(l_1-1)+\sum_{i=l_1}^{l_2-1} f(i,r)\)
需要注意的是以上的式子都是 \(ans+=...\) ,所以 \(ans\) 也要做一个前缀和操作才是正确答案。
照着上面的内容实现就好了,时间复杂度依然是 \(O((n+m)\sqrt n)\) 。
#include<bits/stdc++.h>
#define reg register
#define LL long long
#define pb push_back
#define rep(i,a,b) for(int i=a,i##end=b;i<=i##end;++i)
using namespace std;
bool Handsome;
inline int rd(){
reg int x=0;reg char o=getchar();reg bool O=0;
for(;o<48 || 57<o;o=getchar())if(o=='-')O=1;
for(;48<=o && o<=57;o=getchar())x=(x<<1)+(x<<3)+(o^48);
return O?-x:x;
}
const int M=1e5+5,T=16383;
int n,m,k,sn,a[M],bl[M],sz[T+5],sum[T+5],l,r;
LL ans[M],f[M][2];
struct node{int x,y,z,id;}A[M];
vector<node> g[M];
bool cmp(node a,node b){
if(bl[a.x]==bl[b.x])return bl[a.x]&1?a.y<b.y:a.y>b.y;
return a.x<b.x;
}
vector<int> v;
bool Most;
int main(){
// printf("%.2lfMB\n",(&Most-&Handsome)/1024.0/1024.0);
n=rd(),m=rd(),k=rd();sn=sqrt(n);
if(k>14){rep(i,1,m)puts("0");return 0;}
rep(i,1,n)bl[i]=(i-1)/sn+1;
rep(i,1,n)a[i]=rd();
rep(i,1,T)sz[i]=sz[i>>1]+(i&1);
rep(i,0,T)if(sz[i]==k)v.pb(i);
rep(i,1,m)A[A[i].id=i].x=rd(),A[i].y=rd();
sort(A+1,A+m+1,cmp);
l=1;r=0;
rep(i,1,m){
if(l<A[i].x)g[r].pb((node){l,A[i].x-1,-1,A[i].id});
if(l>A[i].x)g[r].pb((node){A[i].x,l-1,1,A[i].id});
l=A[i].x;
if(r<A[i].y)g[l-1].pb((node){r+1,A[i].y,-1,A[i].id});
if(r>A[i].y)g[l-1].pb((node){A[i].y+1,r,1,A[i].id});
r=A[i].y;
}
rep(i,1,n){
f[i][0]=f[i-1][0]+sum[a[i]];
rep(j,0,v.size()-1)++sum[a[i]^v[j]];
f[i][1]=f[i-1][1]+sum[a[i]];
rep(j,0,g[i].size()-1){
node tmp=g[i][j];
rep(p,tmp.x,tmp.y)ans[tmp.id]+=tmp.z*sum[a[p]];
}
}
l=1;r=0;
rep(i,1,m){
ans[A[i].id]+=ans[A[i-1].id];
ans[A[i].id]+=f[A[i].x-1][1]-f[l-1][1];l=A[i].x;
ans[A[i].id]+=f[A[i].y][0]-f[r][0];r=A[i].y;
}
rep(i,1,m)printf("%lld\n",ans[i]);
return 0;
}
\(\mathcal{By}\quad\mathcal{Most}\ \mathcal{Handsome}\)
本文来自博客园,作者:Most_Handsome,转载请注明原文链接:https://www.cnblogs.com/MostHandsome/p/15089123.html

浙公网安备 33010602011771号