详解莫队
基础莫队
题目。
翻译:给出一个序列,对于每次询问,回答询问区间内不同数字的个数。
如果用暴力,只需要创建一个 cnt 数组,记录当前每个数出现了多少次,cnt[i] 表示 \(i\) 这个数出现的次数,然后循环统计,如果这个数是第一次出现,就加一。
现在我们先将所有询问读入,再将询问按右端点小到大排序,然后暴力求出第一个询问的 cnt,那么就可以得到第一个询问的 res。对于下一个询问,我们将第一个询问的右端点一个个后移,直到等于下一个询问的右端点。如果现在移到的数是 \(x\),则分两种情况:
-
cnt[x]==0,那么cnt[x]++,res++。 -
cnt[x]>0,那么cnt[x]++。
这样右端点最坏只会移 \(n\) 次,但是左端点呢?
我们可以用分块的思想,将整个序列分成 \(\sqrt n\) 块,现在把排序改为:以左端点所在的块的编号为第一关键字,右端点下标为第二关键字排序。这样就可以把时间复杂度降到 \(O(m\sqrt n)\)。
注:由于莫队不是本题的“正解”,所以需要的优化极多,且需要开启 O2 优化。
参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=1000010,M=1000010,S=1000010;
int n,m,len;
int w[N],ans[M];
int tmp[S],idx;
int res;
struct query{//询问
int id,l,r;
}q[M];
int cnt[S];
inline int read(){//快读
register int k=0,f=1;
register char c=getchar_unlocked();
while(c<'0' || c>'9'){
if(c=='-') f=-1;
c=getchar_unlocked();
}
while(c>='0' && c<='9'){
k=k*10+c-'0';
c=getchar_unlocked();
}
return k*f;
}
void write(register int x){//快写
if(x<10) putchar(x+'0');
else{
write(x/10);
putchar(x%10+'0');
}
}
inline bool cmp(register query &a,register query &b){
if(a.l/len!=b.l/len) return a.l/len<b.l/len;//按左端点所在块的编号排
if(a.l/len&1) return a.r<b.r;//玄学优化:奇数块小到大排,偶数块大到小排
return a.r>b.r;
}
/*void add(int x,int &res){//加入一个数
if(cnt[x]==0) res++;//一开始没有
cnt[x]++;
}
void del(int x,int &res){//删除一个数
cnt[x]--;
if(cnt[x]==0) res--;//删完后没了
}*/
int main(){
n=read();
for(register int i=1;i<=n;i++){
w[i]=read();
if(!tmp[w[i]]) tmp[w[i]]=++idx;//使数组访问更连续
w[i]=tmp[w[i]];
}
m=read();
len=max(1,int(n/sqrt(m)));//一定要取max,否则可能RE
for(register int i=0;i<m;i++){
q[i].l=read();
q[i].r=read();
q[i].id=i;
}
sort(q,q+m,cmp);
for(register int k=0,i=0,j=1;k<m;k++){
while(i<q[k].r) res+=(!cnt[w[++i]]++);//add(w[++i],res),右指针右移,扩充
while(i>q[k].r) res-=(!--cnt[w[i--]]);//del(e[i--],res),右指针左移,减少
while(j<q[k].l) res-=(!--cnt[w[j++]]);//del(w[j++],res),左指针右移,减少
while(j>q[k].l) res+=(!cnt[w[--j]]++);//add(w[--j],res),左指针左移,扩充
ans[q[k].id]=res;
}
for(register int i=0;i<m;i++){
write(ans[i]);
putchar('\n');
}
return 0;
}
带修莫队
题目。
相较于上一个题,此题多了修改操作。
我们可以加一个时间戳 \(t\),表示进行完第 \(t\) 次修改后,序列的情况。这样移指针的时候,就要移三个。移 \(l,r\) 不变,如果移时间戳 \(t\),就会分两种情况:
-
修改的在区间 \([l,r]\) 内,则删除原来的数,添加新修改的数。时间复杂度 \(O(1)\)。
-
修改的不在区间 \([l,r]\) 内,则不发生变化。
完成后,由于 \(t\) 有可能再返回,所以我们将两个数交换一下。比如说要将 \(x\) 修改成 \(x'\),就交换 \(t\pm 1\) 的 \(x'\) 与 \(t\) 的 \(x\)。
综上,移 \(t\) 指针也是 \(O(1)\) 的。
对于排序,我们将 \(l\) 所在块的编号作为第一关键字,\(r\) 所在块的编号作为第二关键字,\(t\) 作为第三关键字。
推荐使用 \(n^{\frac{2}{3}}\),也就是 \(\sqrt[3]{n^2}\) 作为块长。
参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=200010,S=1000010;
int n,m;
int len;
int w[N],cnt[S],ans[N];
int mq,mc;//操作数量
struct query{
int id;
int l,r;
int t;//时间戳
}q[N];
struct modify{//修改操作
int p,c;
}c[N];
int get(int x){
return x/len;
}
bool cmp(query a,query b){
int al=get(a.l),ar=get(a.r),bl=get(b.l),br=get(b.r);
if(al!=bl) return al<bl;
if(ar!=br) return ar<br;
return a.t<b.t;
}
void add(int x,int &res){
if(cnt[x]==0) res++;
cnt[x]++;
}
void del(int x,int &res){
cnt[x]--;
if(cnt[x]==0) res--;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++) scanf("%d",&w[i]);
for(int i=0;i<m;i++){
char op[2];
int a,b;
scanf("%s%d%d",op,&a,&b);
if(*op=='Q') q[++mq]={mq,a,b,mc};
else c[++mc]={a,b};
}
len=max(pow(n,0.666),1.0);
sort(q+1,q+mq+1,cmp);
for(int i=0,j=1,t=0,k=1,res=0;k<=mq;k++){
int id=q[k].id,l=q[k].l,r=q[k].r,tm=q[k].t;
while(i<r) add(w[++i],res);
while(i>r) del(w[i--],res);
while(j<l) del(w[j++],res);
while(j>l) add(w[--j],res);
while(t<tm){//执行下一个操作
t++;
if(c[t].p>=j && c[t].p<=i){//在范围内
del(w[c[t].p],res);
add(c[t].c,res);
}
swap(w[c[t].p],c[t].c);//交换以支持逆操作
}
while(t>tm){//撤销当前操作
if(c[t].p>=j && c[t].p<=i){
del(w[c[t].p],res);
add(c[t].c,res);
}
swap(w[c[t].p],c[t].c);
t--;
}
ans[id]=res;
}
for(int i=1;i<=mq;i++) printf("%d\n",ans[i]);
return 0;
}
回滚莫队
如果莫队在维护时,插入操作很好维护,但是删除操作不好维护,那么就需要回滚莫队。
题目。
题目大意:每次给出一个区间,区间里每种数都会又一个重要度:数本身乘这个数在区间内的出现次数。现在要求区间内重要度的最大值。
如果插入一个数 \(x\),就将重要度加 \(x\),再去更新最大值即可。但是如果删除一个数 \(x\),重要度减去 \(x\),最大值就不容易维护了(大佬们可能直到做法,但是本蒟蒻不会)。
询问排序和基础莫队一样。
我们先暴力处理左端点与右端点在一个块内的询问,再处理其他询问。比如现在查询的区间为 \([l,r]\),\(l\) 所在块为 \(B\),\(B\) 的下一个块为 \(B_n\)。对于 \(B\) 内的询问,直接暴力即可;对于跨块询问,先将左端点移到 \(B\) 的最后,右端点移到 \(B_n\) 的开头,下面再正常移左右端点即可,这样就能回避删除操作。
上面可能比较难懂,而且细节不多,所以直接看代码,我会在代码中详细注释:
#include<bits/stdc++.h>
#define LL long long
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=100010;
int n,m;
int len;
int w[N],cnt[N];
LL ans[N];
struct query{
int id,l,r;
}q[N];
vector<int> nums;//离散化
int get(int x){
return x/len;
}
bool cmp(query a,query b){
int i=get(a.l),j=get(b.l);
if(i!=j) return i<j;
return a.r<b.r;
}
void add(int x,LL &res){
cnt[x]++;
res=max(res,(LL)cnt[x]*nums[x]);//一定注意离散化
}
int main(){
cin>>n>>m;
len=sqrt(n);
for(int i=1;i<=n;i++){
scanf("%d",&w[i]);
nums.push_back(w[i]);
}
sort(nums.begin(),nums.end());//离散化排序
nums.erase(unique(nums.begin(),nums.end()),nums.end());//离散化去重
for(int i=1;i<=n;i++)
w[i]=lower_bound(nums.begin(),nums.end(),w[i])-nums.begin();//将每个数换成离散化后的值
for(int i=0;i<m;i++){
scanf("%d%d",&q[i].l,&q[i].r);
q[i].id=i;
}
sort(q,q+m,cmp);//正常排序
for(int x=0;x<m;){
int y=x;
while(y<m && get(q[y].l)==get(q[x].l)) y++;//将所有左端点在一个块内的询问处理完
int right=get(q[x].l)*len+len-1;//左端点所在块的最后一个数
while(x<y && q[x].r<=right){//右端点在块内,即块内查询(由于排序方法,块内询问一定连续)
LL res=0;
int id=q[x].id,l=q[x].l,r=q[x].r;
for(int k=l;k<=r;k++) add(w[k],res);//暴力
ans[id]=res;
for(int k=l;k<=r;k++) cnt[w[k]]--;//还原,以确保每次操作的cnt都是原状态
x++;
}
LL res=0;
int i=right,j=right+1;//左端点没变,所以right不用更新
while(x<y){//剩下的都是跨块的,且右端点递增
int id=q[x].id,l=q[x].l,r=q[x].r;
while(i<r) add(w[++i],res);//移右端点
LL backup=res;//备份,以便下次处理询问(左端点只是在一个块内,但是块内顺序时乱的)
while(j>l) add(w[--j],res);
ans[id]=res;
while(j<right+1) cnt[w[j++]]--;
res=backup;
x++;
}
mems(cnt,0);
}
for(int i=0;i<m;i++) printf("%lld\n",ans[i]);
return 0;
}
树上莫队
题目。
样例树:

我们将整棵树变成一个欧拉序列,就是深度优先遍历序列,从上面到这个点时记录一遍,从这个点离开时再记录一遍。比如样例的欧拉序列就是:1 2 2 3 5 5 6 6 7 7 3 4 8 8 4 1。
定义两个数组 first 和 last,first[u] 表示 \(u\) 第一次出现的位置,last[u] 表示 \(u\) 最后一次(也就是第二次)出现的位置。如果询问从 \(x\) 到 \(y\) 的路径,先判断 first[x] 是不是小于 first[y],如果不是,就互换 \(x,y\)。然后分两种情况讨论:
-
\(\text{lca}(x,y)=x\),则
first[x]到first[y]中只出现一次的点,就是 \(x\) 到 \(y\) 的路径。 -
\(\text{lca}(x,y)\ne x\),则路径对应欧拉序列中从
last[x]到first[y]中只出现一次的点,以及 \(\text{lca}(x,y)\)。
现在问题就转化为:区间中只出现一次的数有多少种不同的权值。其实只需要再加入一个 st 数组,表示每个点是否出现了偶数次。现在小改一下 add 函数:
void add(int x,int &res){
st[x]=!st[x];
if(!st[x]){//删除一个数
cnt[w[x]]--;
if(cnt[w[x]]==0) res--;
}
else{//添加一个数
if(cnt[w[x]]==0) res++;
cnt[w[x]]++;
}
}
不难发现,在这个问题中,add 和 del 其实是一样的操作。后面直接套基础莫队即可。
总结步骤:
-
离散化。
-
DFS 求欧拉序列。
-
倍增求 LCA。
-
将树上询问变成序列中询问。
-
莫队处理。
参考代码:
#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=100010;
int n,m;
int len;
int w[N];
int h[N],e[N],ne[N],idx;
int top;
int dep[N],f[N][20];
int seq[N],first[N],last[N];
int cnt[N],ans[N];
bool st[N];
int que[N];
struct query{
int id,l,r,p=0;
}q[N];
vector<int> nums;
void addedge(int a,int b){
e[idx]=b;
ne[idx]=h[a];
h[a]=idx++;
}
void dfs(int u,int fa){
seq[++top]=u;//从上面到这个点
first[u]=top;
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(j!=fa) dfs(j,u);
}
seq[++top]=u;//从这个点离开
last[u]=top;
}
void bfs(){
mems(dep,0x3f);
dep[0]=0;
dep[1]=1;
int hh=0,tt=0;
que[0]=1;
while(hh<=tt){
int t=que[hh++];
for(int i=h[t];~i;i=ne[i]){
int j=e[i];
if(dep[j]>dep[t]+1){
dep[j]=dep[t]+1;
f[j][0]=t;
for(int k=1;k<=15;k++) f[j][k]=f[f[j][k-1]][k-1];
que[++tt]=j;
}
}
}
}
int lca(int a,int b){
if(dep[a]<dep[b]) swap(a,b);
for(int k=15;k>=0;k--)
if(dep[f[a][k]]>=dep[b]) a=f[a][k];
if(a==b) return a;
for(int k=15;k>=0;k--)
if(f[a][k]!=f[b][k]){
a=f[a][k];
b=f[b][k];
}
return f[a][0];
}
int get(int x){
return x/len;
}
bool cmp(query a,query b){
int i=get(a.l),j=get(b.l);
if(i!=j) return i<j;
return a.r<b.r;
}
void add(int x,int &res){
st[x]=!st[x];
if(!st[x]){
cnt[w[x]]--;
if(cnt[w[x]]==0) res--;
}
else{
if(cnt[w[x]]==0) res++;
cnt[w[x]]++;
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
scanf("%d",&w[i]);
nums.push_back(w[i]);
}
sort(nums.begin(),nums.end());
nums.erase(unique(nums.begin(),nums.end()),nums.end());
for(int i=1;i<=n;i++)
w[i]=lower_bound(nums.begin(),nums.end(),w[i])-nums.begin();
mems(h,-1);
for(int i=0;i<n-1;i++){
int a,b;
scanf("%d%d",&a,&b);
addedge(a,b);
addedge(b,a);
}
dfs(1,-1);
bfs();//预处理
for(int i=0;i<m;i++){
int a,b;
scanf("%d%d",&a,&b);
if(first[a]>first[b]) swap(a,b);//保证a在前,b在后
int p=lca(a,b);
if(a==p) q[i]={i,first[a],first[b]};
else q[i]={i,last[a],first[b],p};
}
len=sqrt(top);
sort(q,q+m,cmp);
for(int k=0,i=1,j=0,res=0;k<m;k++){
int id=q[k].id,l=q[k].l,r=q[k].r,p=q[k].p;
while(j<r) add(seq[++j],res);
while(j>r) add(seq[j--],res);//加入、删除都一样
while(i<l) add(seq[i++],res);
while(i>l) add(seq[--i],res);
if(p!=0) add(p,res);
ans[id]=res;
if(p!=0) add(p,res);//删掉p
}
for(int i=0;i<m;i++) printf("%d\n",ans[i]);
return 0;
}
二次离线莫队
题目。
定义配对:指符合题意的数对。
设 \(L,R\) 为现在的左右端点,\(r\) 为右端点目标位置,那么现在就应该将 \(R+1\) 加入区间。假设 \([L,R]\) 这个区间的答案已经维护好了,那么影响的数对必定包含 \(R+1\) 这个点。所以只需要求 \([L,R]\) 这个区间中有多少个数和 \(R+1\) 配对。
可以用前缀和解决这个问题:定义 \(S_i\) 为 \([1,i]\) 中有多少个数与 \(w_{R+1}\) 配对,现在就可以将新增的答案转化为 \(S_R-S_{L-1}\)。但是显然 \([1,L-1]\) 这个区间和 \(R+1\) 没有什么关系,所以我们将两个 \(S\) 看成两个问题:
-
解决 \(S_R\):定义 \(f(i)\) 表示 \([1,i]\) 中有多少个数与 \(w_{i+1}\) 配对,那么 \(S_R=f(R)\)。设 \(g(x)\) 表示前 \(i\) 个数有多少个数与 \(x\) 配对。现在 \(S_R=f(R)=g(w_{R+1})\),直接暴力维护 \(g\) 即可。设 \(y\) 表示所有满足题意的结果,设当前新增 \(w_i\) 这个数,就要求哪些 \(x\) 满足 \(w_i \oplus x=y_j\),即求有多少个 \(x\) 满足 \(x=w_i \oplus y_j\)。直接遍历所有 \(y_j\),并将 \(g(y_j \oplus w_i)\) 加一就可以。
-
解决 \(S_{L-1}\):我们知道,\(\forall x\in[R+1,r]\) 都需要求一下 \([1,L-1]\) 中有多少个数与 \(x\) 配对,需要很长时间。但是注意到 \([1,L-1]\) 这个区间是不变的,所以可以将“某个区间中每个数与某个固定前缀中有多少对数配对”这种问题拎出来,再离线去做,就可以类比上面的方法。
参考代码:
#include<bits/stdc++.h>
#define LL long long
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=100010;
int n,m,k;
int len;
int w[N];
LL ans[N];
struct query{
int id,l,r;
LL res;//每个询问的结果需要单独存
}q[N];
struct range{//第二次离线
int id,l,r,t;
};
vector<range> ran[N];
int f[N],g[N];
int getcnt(int x){//求x的二进制表示中有几个1
int res=0;
while(x){
res+=(x&1);
x>>=1;
}
return res;
}
int get(int x){
return x/len;
}
bool cmp(query a,query b){
int i=get(a.l),j=get(b.l);
if(i!=j) return i<j;
return a.r<b.r;
}
int main(){
cin>>n>>m>>k;
for(int i=1;i<=n;i++) scanf("%d",&w[i]);
vector<int> nums;//配对出来的结果
for(int i=0;i<1<<14;i++)
if(getcnt(i)==k) nums.push_back(i);
for(int i=1;i<=n;i++){//预处理
for(auto y:nums) g[w[i]^y]++;
f[i]=g[w[i+1]];
}
for(int i=0;i<m;i++){
int l,r;
scanf("%d%d",&l,&r);
q[i]={i,l,r};
}
len=sqrt(n);
sort(q,q+m,cmp);
for(int i=0,L=1,R=0;i<m;i++){
int l=q[i].l,r=q[i].r;
if(R<r) ran[L-1].push_back({i,R+1,r,-1});//存下需要离线的询问
while(R<r) q[i].res+=f[R++];//先处理不用离线的询问
if(R>r) ran[L-1].push_back({i,r+1,R,1});
while(R>r) q[i].res-=f[--R];
if(L<l) ran[R].push_back({i,L,l-1,-1});
while(L<l){
q[i].res+=f[L-1]+!k;//因为L-1相较于L少了L,所以要判断自己是否和自己配对,又因为自己异或自己等于0,所以当且仅当k=0时自己和自己配对
L++;
}
if(L>l) ran[R].push_back({i,l,L-1,1});
while(L>l){
q[i].res-=f[L-2]+!k;
L--;
}
}
mems(g,0);
for(int i=1;i<=n;i++){//处理range中的询问
for(auto y:nums) g[w[i]^y]++;
for(auto &rg:ran[i]){
int id=rg.id,l=rg.l,r=rg.r,t=rg.t;
for(int x=l;x<=r;x++) q[id].res+=g[w[x]]*t;
}
}
for(int i=1;i<m;i++) q[i].res+=q[i-1].res;//注意res存的是新增值
for(int i=0;i<m;i++) ans[q[i].id]=q[i].res;
for(int i=0;i<m;i++) printf("%lld\n",ans[i]);
return 0;
}

浙公网安备 33010602011771号