莫队全家桶
莫队
贴一个神仙博客:莫队全家桶
莫队算法是对询问进行分块的一种算法,其本质是对暴力的优化。
这个算法主要是解决区间操作的,适用于求解那种区间
莫队算法核心思想就是:对于所有查询的区间,通过合理的排序,让两个指针进行快速区间移动,达到求解所有询问的目的,需要离线操作
对于这个合理的排序,一个合理的方案是,对于所有的左端点进行分块,按块递增排序,并且对于右端点块内排序,这样做的目的是可以保证左右端点每一次在块内最多只会移动
模版大致长这样
struct node{
int l, r;
int id;//表示这个询问是第几个,由于询问在排序后顺序会乱掉,我们要存储其原先的询问顺序。
}ask[50005];
bool cmp(node a,node b){
return pos[a.l]==pos[b.l]?a.r<b.r:a.l<b.l;
}
for(int i=1,l=1,r=0;i<=m;i++){//初始化为空区间
while(l > q[i].l) add(a[-- l]);
while(r < q[i].r) add(a[++ r]);
while(l < q[i].l) del(a[l ++]);
while(r > q[i].r) del(a[r --]);
ans[q[i].id]=……;
}
一个小优化是奇偶性排序,具体的只需要更改
bool cmp(node a,node b){
return pos[a.l]==pos[b.l]?(pos[a.l]&1?a.r<b.r:a.r>b.r):a.l<b.l;
}
来看一道例题
XOR and Favorite Number
题面翻译
- 给定一个长度为
的序列 ,然后再给一个数字 ,再给出 组询问,每组询问给出一个区间,求这个区间里面有多少个子区间的异或值为 。
- , , 。
Translated by @char32_t,Reformed by @明依。
样例 #1
样例输入 #1
6 2 3
1 2 1 1 0 3
1 6
3 5
样例输出 #1
7
0
样例 #2
样例输入 #2
5 3 1
1 1 1 1 1
1 5
2 4
1 3
样例输出 #2
9
4
4
分析
看到异或区间和,条件反射式想到异或前缀和,那么我们首先构造原序列的异或前缀和
假设当前的需要增删的是区间端点是x^y=k
,可以推出y=k^x
,此题就变成了一道莫队的查询某个值出现的次数,由于值域在
需要注意的是,异或前缀和查询区间异或和是需要左端点减一的,解决方案可以让所有的区间的左端点向左移动一位。
还有一些细节:在增加一个数进入区间的时候得先统计再插入,在删除的时候要先统计再删除
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#define int long long
using namespace std;
#define N 100050
struct node {
int l, r, id;
}ask[N];
int n, m, block, ans[N], cnt[25*N], s[N], sum, k;
inline int get(int x) {
return (x-1) / block+1;
}
bool cmp(node a, node b) {
return get(a.l) == get(b.l) ? (get(a.l) & 1 ? a.r<b.r : a.r>b.r) : a.l < b.l;
}
void init() {
cin >> n >> m >> k;
block = sqrt(n);
for (int i = 1; i <= n; i++) {
cin >> s[i];
s[i] ^= s[i - 1];
}
for (int i = 1; i <= m; i++) {
cin >> ask[i].l >> ask[i].r;
ask[i].id = i, ask[i].l--;
}
sort(ask + 1, ask + m + 1, cmp);
}
void add(int x) {
sum += cnt[s[x] ^ k];
cnt[s[x]]++;
}
void del(int x) {
cnt[s[x]]--;
sum -= cnt[s[x] ^ k];
}
signed main() {
ios::sync_with_stdio(false);
init();
int l = 1, r = 0;
for (int i = 1; i <= m; i++) {
while (l > ask[i].l)add(--l);
while (r < ask[i].r)add(++r);
while (l < ask[i].l)del(l++);
while (r > ask[i].r)del(r--);
ans[ask[i].id] = sum;
}
for (int i = 1; i <= m; i++) {
cout << ans[i] << endl;
}
}
莫队算法的变形
带修莫队
没想到吧,这玩意还能带修,但只能单点修改
带修莫队的原理是:给每一个询问加一个时间戳
- 首先设计出此题若不考虑修改,普通莫队怎么做
- 对每一个查询打上时间戳
- 正常莫队,在每一次莫队的调整
后,调整 这一维度,对于修改的位置直接暴力修改 - 需要注意,修改的值与原序列的值要交换,因为
可能会滚回来,改回来
需要注意的是,我们将
光说不练假把式,上模版
void change(int l,int r,int t){//表示当前询问区间是l,r,需要进行第$t$次更改
int id=b[t].x;//更改位置
if(l<=id&&id<=r){
del(s[id]);
add(b[t].y);
}
swap(s[id],b[t].y);//必须交换,有可能这个位置会改回来
}
int get(int x){return (x-1)/block+1;}
bool cmp(node a,node b){
return get(a.l)==get(b.l)?(get(a.r)==get(b.r)?a.t<b.t:a.r<b.r):a.l<b.l;
}
//main函数中
for(int i=1;i<=n;i++)cin>>s[i];//原序列
for(int i=1;i<=m;i++){
int opt,l,r;
cin>>opt>>l>>r;
if(opt==1)b[++lst]={l,r};//修改操作,对位置为l的数进行r的修改
else a[++tot]={l,r,lst,tot};//查询操作
}
sort(a+1,a+tot+1,cmp);//排序
int l=1,r=0,t=0;
for(int i=1;i<=tot;i++){
while(l<a[i].l)del(s[l++]);
while(l>a[i].l)add(s[--l]);
while(r>a[i].r)del(s[r--]);
while(r<a[i].r)add(s[++r]);
while(t<a[i].t)change(l,r,++t);
while(t>a[i].t)change(l,r,t--);
ans[a[i].id]=sum;
}
来分析分析复杂度:
设块长为
来两道例题:
P2464
题目描述
小 J 是国家图书馆的一位图书管理员,他的工作是管理一个巨大的书架。虽然他很能吃苦耐劳,但是由于这个书架十分巨大,所以他的工作效率总是很低,以致他面临着被解雇的危险,这也正是他所郁闷的。
具体说来,书架由
小 J 的工作有两类:
-
图书馆经常购置新书,而书架任意时刻都是满的,所以只得将某位置的书拿掉并换成新购的书。
-
小 J 需要回答顾客的查询,顾客会询问某一段连续的书位中某一特定编码的书有多少本。
例如,共
一位顾客询问书位
一位顾客询问书位
此时,图书馆购进一本编码为“
一位顾客询问书位
一位顾客询问书位
……
你的任务是写一个程序来回答每个顾客的询问。
输入格式
第一行两个整数
接下来一行共
接下来
若字符为 C
,表示图书馆购进新书,后接两个整数
若字符为 Q
,表示一个顾客的查询,后接三个整数
输出格式
对每一个顾客的查询,输出一个整数,表示顾客所要查询的结果。
样例 #1
样例输入 #1
5 5
1 2 3 4 5
Q 1 3 2
Q 1 3 1
C 2 1
Q 1 3 2
Q 1 3 1
样例输出 #1
1
1
0
2
提示
对于
对于
对于
分析
一句话题意:给定一段区间,支持单点修改和区间定值出现数量查询
套路:离线,离散化,开一个计数数组计数,然后就变成了板子。。。
例2
题意:给定一个序列,有两种操作
1 x y
表示将位置为x
的数改为y
2 l r
表示查询区间
提示:值域与
分析
很明显开个桶记录每个数出现次数,那么对于add,del,change
三个函数的设置就很简单了。具体地,
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
#define N 300500
struct node{
int l,r,t,id;
}a[N];
struct Node {
int x,y;
}b[N];
int block;
int get(int x){
return x/block;
}
int n,m,cnt[N],tot,ans[N],s[N],sum,lst;
bool cmp(node a,node b){
return get(a.l)==get(b.l)?(get(a.r)==get(b.r)?a.t<b.t:a.r<b.r):a.l<b.l;
}
void add(int x){
if(cnt[x]==0)sum++;
if(cnt[x]==1)sum--;
cnt[x]++;
}
void del(int x){
if(cnt[x]==1)sum--;
if(cnt[x]==2)sum++;
cnt[x]--;
}
void change(int l,int r,int t){
int id=b[t].x;
if(l<=id&&id<=r){
del(s[id]);
add(b[t].y);
}
swap(s[id],b[t].y);
}
int main(){
ios::sync_with_stdio(false);
cin>>n>>m;
memset(ans,-1,sizeof ans);
block=pow(n,0.67);
for(int i=1;i<=n;i++)cin>>s[i];
for(int i=1;i<=m;i++){
int opt,l,r;
cin>>opt>>l>>r;
if(opt==1)b[++lst]={l+1,r};
else a[++tot]={l+1,r+1,lst,tot};
}
sort(a+1,a+tot+1,cmp);
int l=1,r=0,t=0;
for(int i=1;i<=tot;i++){
while(l<a[i].l)del(s[l++]);
while(l>a[i].l)add(s[--l]);
while(r>a[i].r)del(s[r--]);
while(r<a[i].r)add(s[++r]);
while(t<a[i].t)change(l,r,++t);
while(t>a[i].t)change(l,r,t--);
ans[a[i].id]=sum;
}
for(int i=1;i<=tot;i++)cout<<ans[i]<<"\n";
return 0;
}
例三
Machine Learning
题面翻译
给你一个数组
1、查询区间
2、单点修改某一个位置的值。
mex指的是一些数字中最小的未出现的自然数。值得注意的是,区间
感谢@租酥雨 提供的翻译
题目描述
You come home and fell some unpleasant smell. Where is it coming from?
You are given an array
- You are given two integers
and . Let be the number of occurrences of in , where is the subarray of from -th element to -th inclusive. Find the Mex of - You are given two integers
to . Change to .
The Mex of a multiset of numbers is the smallest non-negative integer not in the set.
Note that in this problem all elements of
输入格式
The first line of input contains two integers
The second line of input contains
Each of the next
The first type of query is described by three integers
The second type of query is described by three integers
输出格式
For each query of the first type output a single integer — the Mex of
样例 #1
样例输入 #1
10 4
1 2 3 1 1 2 2 2 9 9
1 1 1
1 2 8
2 7 1
1 2 8
样例输出 #1
2
3
2
提示
The subarray of the first query consists of the single element —
The subarray of the second query consists of four
The subarray of the fourth query consists of three
分析
感觉一般数据结构根本无法维护,考虑莫队。
先分析如果没有修改怎么办,即add
与del
如何设计
很有用的性质:每个数add
与del
的时候,最多使得这个数出现次数改变mex
,设当前统计出的mex
为add
假设我们加入了数
cnt2[cnt1[s]]--;//此时为0可能会更新mex
cnt1[s]++;
cnt2[cnt1[s]]++;
考虑其对mex
的影响,当第一次减去mex
,但直接暴力求解mex
复杂度如何呢,让我们想想,从1开始扫,最坏情况下,即为有一个数出现1次,一个数出现2次,一个数出现3次……一直到
于是我们就得到了一个看似无比暴力的莫队算法,add,del
完全只需要维护cnt1,cnt2
即可,无需考虑影响,最后当mex
即可。
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
#define N 500050
int a[N],b[N],c[N],ans[N],cnt1[N],cnt2[N],num,n,m,lst,tot,block;
struct node{
int l,r,t,id;
}ask[N];
struct Node{
int x,y;
}upd[N];
inline int get(int x){
return (x-1)/block+1;
}
bool cmp(node a,node b){
return get(a.l)==get(b.l)?(get(a.r)==get(b.r)?a.t<b.t:a.r<b.r):a.l<b.l;
}
void add(int x){
cnt2[cnt1[x]]--;
cnt1[x]++;
cnt2[cnt1[x]]++;
}
void del(int x){
cnt2[cnt1[x]]--;
cnt1[x]--;
cnt2[cnt1[x]]++;
}
void change(int l,int r,int t){
int id=upd[t].x;
if(l<=id&&id<=r){
del(a[id]);
add(upd[t].y);
}
swap(upd[t].y,a[id]);
}
int find(){
for(int i=1;i;i++){
if(!cnt2[i])return i;
}
}
int main(){
ios::sync_with_stdio(false);
cin>>n>>m;
num=n;
block=pow(n,0.67);
for(int i=1;i<=n;i++){
cin>>a[i];
c[i]=a[i];
}
for(int i=1;i<=m;i++){
int opt,l,r;
cin>>opt>>l>>r;
if(opt==1)ask[++tot]={l,r,lst,tot};
else upd[++lst]={l,r},c[++num]=r;
}
sort(c+1,c+num+1);
num=unique(c+1,c+num+1)-c-1;
for(int i=1;i<=n;i++)a[i]=lower_bound(c+1,c+num+1,a[i])-c;
for(int i=1;i<=lst;i++)upd[i].y=lower_bound(c+1,c+num+1,upd[i].y)-c;
sort(ask+1,ask+tot+1,cmp);
for(int i=1,l=1,r=0,t=0;i<=tot;i++){
while(l<ask[i].l)del(a[l++]);
while(l>ask[i].l)add(a[--l]);
while(r<ask[i].r)add(a[++r]);
while(r>ask[i].r)del(a[r--]);
while(t<ask[i].t)change(l,r,++t);
while(t>ask[i].t)change(l,r,t--);
ans[ask[i].id]=find();
}
for(int i=1;i<=tot;i++)printf("%d\n",ans[i]);
}
回滚莫队
由于莫队的核心操作是
只删不增的回滚莫队
这里主要是先暴力统计大区间答案,再通过不断删这个大区间达到只删不增的效果
- 首先将询问的区间以左端点所在块为第一关键字升序,以右端点为第二关键字降序排序
- 对于左右端点都在同一个块的询问,直接暴力统计答案,复杂度为
- 对于左端点在同一个块的询问,将
初始化为这个块的左端点,将 初始化为 ,这部分先暴力统计这段答案,复杂度一般 - 由于右端点降序,在处理这同一个块的询问的时候,右端点单调递减,只用删除
- 由于左端点可能无序,考虑建立
先记录下左端点为块的左端点的答案,然后将左端点向右移动,同样是只用删除,当到达指定位置之后,统计一次答案,然后撤销删除,但不统计答案,当撤销回块左端点时,再将答案重新变为
在实现上,为了代码的方便,可以给左右端点在同一个块的区间单独开一组统计数组来暴力求解
//只删不增回滚莫队伪代码
void del(int x) {
}
void move(int x) {
//删除的逆操作,但不更新答案
}
void solve() {//pos表示所属块
int lst = 0,l=1,r=0;//上个询问所属哪一个块
for (int i = 1; i <= tot; i++) {
if (pos[a[i].l] == pos[a[i].r]) {
//暴力处理块内的询问
ans[a[i].id]=//
//撤销暴力统计的操作
continue;
}
if (lst != pos[a[i].l]) {
lst = pos[a[i].l];//需要再次初始化一次
while (l < L[lst])del(l++);
while (r < n)move(++r);
//暴力计算此时答案
}
while (r > a[i].r)del(r--);
int tmp =/*此时答案*/;
while (l < a[i].l)del(l++);
ans[a[i].id]= ;
while (l > L[pos[a[i].l]])move(--l);
/*此时答案*/ = tmp;
}
}
不难得知,复杂度为
例题:Rmq Problem / mex
描述
有一个长度为
输入
第一行,两个正整数
第二行,
接下来
分析
首先按照套路维护计数数组,然后考虑删除怎么做,很明显,令这个数出现次数减一,减到0就更新答案,那么剩下的都只是套一下即可。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
#define N 500050
#define re register
int n, m, cnt[N], cnt1[N], sum, pos[N], L[N], R[N], s[N], ans[N], block, siz;
struct node {
int l, r, id;
bool operator<(const node& b)const {
return pos[l] == pos[b.l] ? r > b.r : l < b.l;
}
}a[N];
inline void init() {
cin >> n >> m;
block = sqrt(n), siz = n / block + (n % block != 0);
for (re int i = 1; i <= siz; i++)L[i] = R[i - 1] + 1, R[i] = R[i - 1] + block;
R[siz] = n;
for (re int i = 1; i <= siz; i++) {
for (re int j = L[i]; j <= R[i]; j++) {
pos[j] = i;
}
}
for (re int i = 1; i <= n; i++)cin >> s[i];
for (re int i = 1; i <= m; i++) {
cin >> a[i].l >> a[i].r;
a[i].id = i;
}
sort(a + 1, a + m + 1);
return;
}
inline void del(int x) {
cnt[s[x]]--;
if (cnt[s[x]] == 0)sum = min(sum, s[x]);
return;
}
inline void move(int x) {
cnt[s[x]]++;
return;
}
inline void solve() {
int lst = 0, l = 1, r = 0;
for (re int i = 1; i <= m; i++) {
if (pos[a[i].l] == pos[a[i].r]) {
for (re int j = a[i].l; j <= a[i].r; j++) {
cnt1[s[j]]++;
}
for (re int k = 0; k <= a[i].r - a[i].l + 1; k++) {
if (cnt1[k] == 0) {
ans[a[i].id] = k;
break;
}
}
for (re int j = a[i].l; j <= a[i].r; j++) {
cnt1[s[j]]--;
}
continue;
}
if (lst != pos[a[i].l]) {
lst = pos[a[i].l];
while (l < L[lst])del(l++);
while (r < n)move(++r);
for (re int i = 0; i <= n; i++) {
if (!cnt[i]) {
sum = i; break;
}
}
}
while (r > a[i].r)del(r--);
int tmp = sum;
while (l < a[i].l)del(l++);
ans[a[i].id] = sum;
while (l > L[lst])move(--l);
sum = tmp;
}
return;
}
int main() {
// freopen("P4137_4.in","r",stdin);
// freopen("P4137_4.ans","w",stdout);
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
init();
solve();
for (re int i = 1; i <= m; i++)cout << ans[i] << "\n";
return 0;
}
只增不删的回滚莫队
类比只删不增的回滚莫队,容易想到可以这样做:
- 将询问区间按左端点所在块为第一关键字递增排序,右端点为第二关键字递增排序
- 对于每个块,初始化
,这是一个空区间 - 对于左右端点在同一个块内的询问,暴力处理即可
- 否则,对于右端点的处理,由于递增,所以只增不删
- 对于左端点,先记录下此时的答案,然后只增不删往左滚,更新答案之后原路撤销操作
- 将答案还原即可
这里没有处理新开的块,原因是本身是一个空区间
板子大概类似
void add(int x) {
}
void move(int x) {
}
void solve() {
int lst = 0, l = 1, r = 0;
for (int i = 1; i <= tot; i++) {
if (pos[a[i].l] == pos[a[i].r]) {
//用那个另开的统计数组暴力统计答案
and[a[i].id]=
//撤销掉所有操作
continue;
}
if (lst != pos[a[i].l]) {
while (l <= R[pos[a[i].l]])move(l++);
while (r > R[pos[a[i].l]])move(r--);
//重置答案
lst = pos[a[i].l];
}
while (r < a[i].r)add(++r);
int tmp =/*此时答案*/;
while (l > a[i].l)add(--l);
ans[a[i].id] =/*此时答案*/;
/*此时答案*/ = tmp;
while (l <= R[pos[a[i].l]])move(l++);
}
}
例题:洛谷模版
给定一个序列,多次询问一段区间
序列中两个元素的间隔距离指的是两个元素下标差的绝对值。
分析
解法1
考虑回滚莫队的常规操作,由于要求相同元素距离最大值,显然必须对于每一个值都求出相邻元素最大值,而欲求这个最大值,就必须知道当前区间每个值最后出现的位置和最先出现的位置,分别记为
同块暴力很简单,不多说
#include<iostream>
#include<algorithm>
#include<cmath>
#include<vector>
using namespace std;
#define N 5000005
int s[N],n,m,L[N],R[N],pos[N],ans[N],block,siz,sum,st[N],ed[N],st1[N],ed1[N],b[N],c[N],cnt,vis[N];
struct node{
int l,r,id;
bool operator<(const node b ){
return pos[l]==pos[b.l]?r<b.r:l<b.l;
}
}a[N];
void init(){
cin>>n;
for(int i=1;i<=n;i++)cin>>s[i];
for(int i=1;i<=n;i++)c[i]=s[i];
sort(c+1,c+n+1);
cnt=unique(c+1,c+n+1)-c-1;
for(int i=1;i<=n;i++){
s[i]=lower_bound(c+1,c+cnt+1,s[i])-c;
// cout<<s[i]<<" ";
}
//cout<<endl;
cin>>m;
for(int i=1;i<=m;i++){
int l,r;
cin>>l>>r;
a[i]={l,r,i};
}
block=sqrt(n);
siz=n/block+(n%block!=0);
for(int i=1;i<=siz;i++)L[i]=R[i-1]+1,R[i]=R[i-1]+block;
R[siz]=n;
for(int i=1;i<=siz;i++){
for(int j=L[i];j<=R[i];j++){
pos[j]=i;
}
}
sort(a+1,a+m+1);
}
void addr(int x){
ed[s[x]]=x;
if(!st[s[x]])st[s[x]]=x;
sum=max(sum,ed[s[x]]-st[s[x]]);
}
void addl(int x){
if(!ed[s[x]])ed[s[x]]=x;
sum=max(ed[s[x]]-x,sum);
}
void movel(int x){
if(ed[s[x]]==x)ed[s[x]]=0;
}
void solve(){
int l=1,r=0,lst=0;
for(int i=1;i<=m;i++){
if(pos[a[i].l]==pos[a[i].r]){
int sum=0;
for(int j=a[i].l;j<=a[i].r;j++){
if(!st1[s[j]])st1[s[j]]=j;
sum=max(sum,j-st1[s[j]]);
}
ans[a[i].id]=sum;
for(int j=a[i].l;j<=a[i].r;j++){
st1[s[j]]=0;
}
continue;
}
if(lst!=pos[a[i].l]){
lst=pos[a[i].l];
for(int i=l;i<=r;i++)ed[s[i]]=st[s[i]]=0;
l=R[lst]+1,r=R[lst];
sum=0;
}
while(r<a[i].r)addr(++r);
int tmp=sum;
while(l>a[i].l)addl(--l);
ans[a[i].id]=sum;
while(l<=R[lst])movel(l++);
sum=tmp;
}
}
int main(){
ios::sync_with_stdio(false);
init();
solve();
for(int i=1;i<=m;i++)cout<<ans[i]<<"\n";
return 0;
}
解法2
有一说一,此题不用回滚莫队,可以
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)