莫队
莫队是一类离线区间询问问题, 经常应用于需要维护的信息无法合并时(如线段树等)
其核心思想是: 维护两个指针
l
,r
. 在已知[l,r]
这段区间的信息的前提下, 两个指针分别移动到l'
,r'
的过程中, 实时地维护答案, 从而算出区间[l,r]
的信息
莫队之基础莫队
莫队是一类离线区间询问问题, 核心是对大量的询问进行处理, 每个询问一般都有一个区间
[l,r]
, 我们对询问进行分块维护两个指针
l
,r
, 在已知[l,r]
这段区间的信息的前提下, 两个指针分别移动到l'
,r'
的过程中, 实时地维护答案, 从而算出区间[l,r]
的信息
对询问进行分块
① 按照
[l,r]
,l
递增进行排序, 分成 \(\sqrt{n}\) 块② 每一块内部按照
r
排序优化: 分块长度
len
= \(\sqrt{\dfrac{n^2}{m}}\) , (n
为数组长度,m
为询问个数)\(\quad\) \(\quad\) 奇数块内
r
从小到大排序, 偶数块内r
从大到小排序
//基础莫队算法模板
int n,m,len; //n为数组长度,m为询问个数,len为分块长度
int w[N],ans[M],cnt[S]; //w[]记录数组,ans[]记录每个询问答案,cnt[]数组实时维护每个元素出现的次数
struct Query
{
int id,l,r;
}q[M]; //离线记录询问
int get (int l) //按左端点分块
{
return l/len;
}
bool cmp (const Query&a,const Query &b) //对询问排序
{
int i=get(a.l),j=get(b.l);
if(i!=j)return i<j; //第一关键字:左端点l分块从小到大排序
else return a.r<b.r; //第二关键字:同一块内,按右端点r排序
}
void add (int x,int &res)
{
if(!cnt[x])res++;
cnt[x]++;
}
void del (int x,int &res)
{
cnt[x]--;
if(!cnt[x])res--;
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)cin>>w[i];
cin>>m;
len=max(1,(int)sqrt((double)n*n/m));
for(int i=0;i<m;i++)
{
int l,r;
cin>>l>>r;
q[i]={i,l,r};
}
sort(q,q+m,cmp);
for(int k=0,i=0,j=1,res=0;k<m;k++) //i是向r靠齐的指针,j是向l靠齐的指针
{
int id=q[k].id,l=q[k].l,r=q[k].r;
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);
ans[id]=res;
}
for(int i=0;i<m;i++)cout<<ans[i]<<'\n';
return 0;
}
莫队之带修改的莫队
在离线莫队里加入时间戳
(l,r,t)
对于操作来说, 我们把修改和询问分开
对于询问: 左端点所在块为第一关键字, 右端点所在块为第二关键字, 时间为第三关键字进行排序
与普通莫队相似, 只需要多维护一个修改的操作: 假设两个询问的时间分别为
t1
,t2
, 只需要把[t1,t2]
这段时间内的修改操作执行一遍(时光正流或倒流)优化:
len
= \(\sqrt[3]{nt} + 1\) , (n
为元素个数,t
为时间/操作次数)
//带修莫队算法模板
int n,m,mq,mc,len; //n为元素个数,mq为询问次数,mc为操作次数
int w[N],cnt[S],ans[M];
struct Query //记录询问
{
int id,l,r,t;
}q[M];
struct Modify //记录操作
{
int p,c;
}c[M];
int get (int x)
{
return x/len;
}
bool cmp (const Query&a,const Query&b)
{
int al=get(a.l),ar=get(a.r);
int 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])res++;
cnt[x]++;
}
void del (int x,int &res)
{
cnt[x]--;
if(!cnt[x])res--;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>w[i];
for(int i=0;i<m;i++)
{
char op[2];
int a,b;
cin>>op>>a>>b;
if(*op=='Q')mq++,q[mq]={mq,a,b,mc}; //记录询问
else c[++mc]={a,b}; //记录操作
}
len=cbrt((double)n*max(1,mc))+1;
sort(q+1,q+1+mq,cmp);
for(int k=1,i=0,j=1,res=0,t=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>=l&&c[t].p<=r)
{
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>=l&&c[t].p<=r)
{
del(w[c[t].p],res);
add(c[t].c,res);
}
swap(w[c[t].p],c[t].c);
t--;
}
ans[idx]=res;
}
for(int i=1;i<=mq;i++)cout<<ans[i]<<'\n';
return 0;
}
莫队之回滚莫队
回滚莫队用于维护一段区间内的
max
或min
处理一段区间分为两部分:
① 对于左端点
l
和右端点r
在同一段内的区间, 暴力处理② 对于左端点
l
和右端点r
不在同一段内的区间, 分别处理[l,right]
和[right+1,r]
以左端点所在的块升序为第一关键字, 以右端点升序为第二关键字
//回滚莫队算法模板
int n,m,len;
int w[N],cnt[N];
long long ans[N];
vector<int> nums;
struct Query
{
int id,l,r;
}q[N];
int get (int x)
{
return x/len;
}
bool cmp (const Query&a,const Query&b)
{
int i=get(a.l),j=get(b.l);
if(i!=j) return i<j;
else return a.r<b.r;
}
void add (int x,long long &res) //回滚莫队只有增加操作,没有删减操作
{
cnt[x]++;
res=max(res,(long long)cnt[x]*nums[x]);
}
int main()
{
cin>>n>>m;
len=sqrt(n);
for(int i=1;i<=n;i++)cin>>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();
//w[i]存储原数在离散化数组nums中的下标
for(int i=0;i<m;i++)
{
int l,r;
cin>>l>>r;
q[i]={i,l,r};
}
sort(q,q+m,cmp);
for(int x=0;x<m;)
{
int y=x; //处理左端点l在同一段内的所有询问[x,y)
while(y<m&&get(q[y].l)==get(q[x].l))y++;
int right=(get(q[x].l)+1)*len-1; //左端点l所在段终点为right
//暴力求右端点r在块内的询问
while(x<y&&q[x].r<=right)
{
long long 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]]--; //复原
x++;
}
//求右端点r在块外的询问
long long res=0;
int i=right,j=right+1; //i是右指针,j是左指针
while(x<y)
{
int id=q[x].id,l=q[x].l,r=q[x].r;
while(i<r)add(w[++i],res);
long long backup=res; //备份[right+1,r]的res值
while(j>l)add(w[--j],res);
ans[id]=res;
while(j<right+1)cnt[w[j++]]--; //复原
res=backup;
x++;
}
memset(cnt,0,sizeof cnt);
}
for(int i=0;i<m;i++)cout<<ans[i]<<'\n';
return 0;
}
莫队之树上莫队
树上莫队可以在树上查询一些很有意思的东西, 比如众数、数的出现次数
众所周知, 莫队是在序列上进行一系列的查询的, 并且必须离线与静态. 那么我们就不妨考虑将一棵树转化为一个序列, 再在这个序列上进行我们想要的操作. 这个序列就是欧拉序
欧拉序列指的是对一棵树的深度优先遍历的顺序, 在遍历每一个点的时候, 先将这个点放到序列中, 然后递归遍历它的所有子树, 遍历完所有子树后, 回溯时再把这个点放到序列中 (即每个点在序列中都会出现两遍)
欧拉序列的性质:
对于一棵树的欧拉序列,
ein[u]
表示节点u
在欧拉序列中第一次出现的位置,eout[u]
表示节点u
在欧拉序列中最后一次出现的位置对于树上的两个节点
x
,y
, 满足ein[x] < ein[y]
① 若
lca(x,y) == x
, 则从x
到y
的路径为欧拉序列中[ein[x], ein[y]]
中只出现一次的点② 若
lca(x,y) != x
, 则从x
到y
的路径为欧拉序列中[eout[x], ein[y]]
中只出现一次的点以及lca(x, y)
树上莫队一般流程:
① 离散化
② 求欧拉序列
③ 求LCA (倍增/ tarjan)
④ 将树中询问变成序列中的询问
⑤ 基础莫队
//树上莫队算法模板
int n,m,len; //n为节点个数,m为询问个数,len为分块长度
int w[N]; //存储每个节点的元素值
int h[N],e[N],ne[N],idx; //邻接表存储树边
int depth[N],fa[N][size]; //倍增求lca,size取logn(n为节点数量)
int seq[N],top,ein[N],eout[N]; //欧拉序列
int cnt[N],st[N],ans[N];
//cnt[]数组实时维护每个元素出现的次数,st[]数组实时维护每个节点出现的次数,ans[]记录每个询问答案
int que[N]; //宽搜队列
vector<int> nums; //离散化数组
struct Query
{
int id,l,r,p; //p记录是否考虑lca,及lca是多少
}q[N]; //离线记录询问
void add_edge (int a,int b) //添加有向边
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dfs (int u,int father) //深搜预处理欧拉序列
{
seq[++top]=u;
ein[u]=top; //ein[u]表示节点u在欧拉序列中第一次出现的位置
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
if(j!=father)dfs(j,u);
}
seq[++top]=u;
eout[u]=top; //eout[u]表示节点u在欧拉序列中最后一次出现的位置
}
void bfs() //宽搜预处理depth和fa数组
{
memset(depth,0x3f,sizeof depth);
depth[0]=0,depth[1]=1;
int hh=0,tt=0;
que[0]=1;
while(hh<=tt)
{
int t=que[hh++];
for(int i=h[t];i!=-1;i=ne[i])
{
int j=e[i];
if(depth[j]>depth[t]+1)
{
depth[j]=depth[t]+1;
que[++tt]=j;
fa[j][0]=t;
for(int k=1;k<=size;k++)
fa[j][k]=fa[fa[j][k-1]][k-1];
}
}
}
}
int lca (int a,int b) //查询节点a,b的最近公共祖先
{
if(depth[a]<depth[b])swap(a,b);
for(int k=size;k>=0;k--)
if(depth[fa[a][k]]>=depth[b])
a=fa[a][k];
if(a==b)return a;
for(int k=size;k>=0;k--)
if(fa[a][k]!=fa[b][k])
{
a=fa[a][k];
b=fa[b][k];
}
return fa[a][0];
}
int get (int x) //分块
{
return x/len;
}
bool cmp (const Query &a,const Query &b) //对询问排序
{
int i=get(a.l),j=get(b.l);
if(i!=j)return i<j; //第一关键字:左端点l分块按从小到大排序
else return a.r<b.r; //第二关键字:同一块内,按右端点r排序
}
void add (int x,int &res) //树上莫队添加和删减操作相同
{
st[x]^=1;
if(st[x]==0)
{
cnt[w[x]]--;
if(!cnt[w[x]])res--;
}
else
{
if(!cnt[w[x]])res++;
cnt[w[x]]++;
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>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.end();
//w[i]存储原数在离散化数组nums中的下标
memset(h,-1,sizeof h);
for(int i=0;i<n-1;i++)
{
int a,b;
cin>>a>>b;
add_edge(a,b),add_edge(b,a);
}
dfs(1,-1);
bfs();
for(int i=0;i<m;i++)
{
int a,b;
cin>>a>>b;
if(ein[a]>ein[b])swap(a,b);
int p=lca(a,b);
if(a==p)q[i]={i,ein[a],ein[b]};
else q[i]={i,eout[a],ein[b],p};
}
len=sqrt(top);
sort(q,q+m,cmp);
for(int k=0,i=0,j=1,res=0;k<m;k++) //i是向r靠齐的指针,j是向l靠齐的指针
{
int id=q[k].id,l=q[k].l,r=q[k].r,p=q[k].p;
while(i<r)add(seq[++i],res);
while(i>r)add(seq[i--],res);
while(j<l)add(seq[j++],res);
while(j>l)add(seq[--j],res);
if(p)add(p,res);
ans[id]=res;
if(p)add(p,res);
}
for(int i=0;i<m;i++)cout<<ans[i]<<'\n';
return 0;
}
莫队之二次离线莫队
莫队二次离线, 是一种莫队, 专门用来处理莫队中转移不是 \(O(1)\) , 但可以前缀和拆分的问题, 其基于莫队 + 扫描线的思想, 通过扫描线, 再次将更新答案的过程离线处理, 降低时间复杂度
具体的, 设更新答案的时间复杂度为 \(O(k)\) , 那么它可以将莫队的时间复杂度从 \(O(nk\sqrt n)\) 降低到 \(O(nk + n\sqrt n)\) , 大大简化了计算
二次离线莫队通常适用于满足以下条件的题目:
- 可用莫队做
- 莫队扩展或者删除一个点对答案的影响取决于当前区间的长度
- 扩展或删除一个点对答案的影响可用前缀写成差分的形式
莫队二次离线的应用: AcWing 2535. 二次离线莫队
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 100010;
struct Query
{
int id, l, r; //编号、左端点、右端点
LL ans; //由于每个询问的答案可能分成多个部分分别处理,需要先将部分答案单独存下来
}q[N]; //存储所有询问
struct Range
{
int id, l, r, t; //编号、左端点、右端点、类型(1 表示加上,-1 表示减去)
};
//range[i] 表示询问 w[l] ~ w[r] 中每个数和 w[1] ~ w[i] 共有多少个配对
vector<Range> range[N]; //存储每个询问中需要二次离线解决的问题
int n, m, k, len;
int w[N]; //原序列
int id[N]; //记录每个下标所在的块编号
LL res[N]; //记录每个询问的答案
//f[i] 表示 w[1] ~ w[i] 中与 w[i + 1] 配对的数的个数
//g[x] 表示前 i 个数中与 x 配对的数的个数
int f[N], g[N];
inline int get(int x) //分块
{
return x / len;
}
inline int get_count(int x) //计算 x 的二进制表示中 1 的个数
{
int res = 0;
for(int i = 0; i < 14; i++) res += x >> i & 1;
return res;
}
//先按照左端点所在块的编号从小到大排序,再按照右端点从小到大排序
bool cmp(const Query &a, const Query &b)
{
int i = get(a.l), j = get(b.l);
if(i != j) return i < j;
return a.r < b.r;
}
int main()
{
scanf("%d%d%d", &n, &m, &k);
for(int i = 1; i <= n; i++) scanf("%d", &w[i]);
vector<int> nums; //存储所有二进制表示中有 k 个 1 的数
for(int i = 0; i < 1 << 14; i++) //枚举范围内的所有数
if(get_count(i) == k) //如果当前数的二进制表示中有 k 个 1
nums.push_back(i); //将当前数加入容器
//预处理 f[]
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 x = 0, i = 1, j = 0; x < m; x++)
{
int l = q[x].l, r = q[x].r;
//j 向右移向 r,则 [j + 1, r] 中的所有 -S[i - 1] 需要二次离线处理
if(j < r) range[i - 1].push_back({x, j + 1, r, -1});
while(j < r) q[x].ans += f[j++];
//j 向右移向 r,加入 w[j + 1],ans + (S[j] - S[i - 1])
//j 向左移向 r,则 [r + 1, j] 中的所有 +S[i - 1] 需要二次离线处理
if(j > r) range[i - 1].push_back({x, r + 1, j, 1});
while(j > r) q[x].ans -= f[--j];
//j 向左移向 r,删去 w[j],ans - (S[j - 1] - S[i - 1])
//i 向右移向 l,则 [i, l - 1] 中的所有 -S[j] 需要二次离线处理
if(i < l) range[j].push_back({x, i, l - 1, -1});
while(i < l) q[x].ans += f[i - 1] + !k, i++;
//i 向右移向 l,删去 w[i],ans - (S[j] - S[i])
//i 向左移向 l,则 [l, i - 1] 中的所有 +S[j] 需要二次离线处理
if(i > l) range[j].push_back({x, l, i - 1, 1});
while(i > l) q[x].ans -= f[i - 2] + !k, i--;
//i 向左移向 l,加入 w[i - 1],ans + (S[j] - S[i - 1])
}
memset(g, 0, sizeof g); //g[] 数组复用,提前清空
//二次离线
for(int i = 1; i <= n; i++)
{
for(auto y: nums) g[w[i] ^ y]++; //递推出前 i 个数的 g[]
for(auto& rg: range[i]) //处理所有和 1 ~ i 求配对的询问
{
int id = rg.id, l = rg.l, r = rg.r, t = rg.t;
for(int x = l; x <= r; x++) q[id].ans += g[w[x]] * t;
//将求出的结果加入对应询问的答案中
}
}
for(int i = 1; i < m; i++) q[i].ans += q[i - 1].ans; //前缀和求出所有询问的最终答案
for(int i = 0; i < m; i++) res[q[i].id] = q[i].ans; //记录所有询问的答案
for(int i = 0; i < m; i++) printf("%lld\n", res[i]); //输出所有询问的答案
return 0;
}