莫队全家桶

莫队

贴一个神仙博客:莫队全家桶
莫队算法是对询问进行分块的一种算法,其本质是对暴力的优化。

这个算法主要是解决区间操作的,适用于求解那种区间\([l,r]\)可以快速支持区间的端点移动\(+1,-1\)的问题,也是充分利用已知信息,避免重复计算的典范

莫队算法核心思想就是:对于所有查询的区间,通过合理的排序,让两个指针进行快速区间移动,达到求解所有询问的目的,需要离线操作

对于这个合理的排序,一个合理的方案是,对于所有的左端点进行分块,按块递增排序,并且对于右端点块内排序,这样做的目的是可以保证左右端点每一次在块内最多只会移动\(O(\sqrt n)\),并且也最多跳\(\sqrt n\)个块,总复杂度为\(O(n\sqrt n)\)

模版大致长这样

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]=……;
}

一个小优化是奇偶性排序,具体的只需要更改\(cmp\)即可

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

题面翻译

  • 给定一个长度为\(n\)的序列\(a\),然后再给一个数字\(k\),再给出\(m\)组询问,每组询问给出一个区间,求这个区间里面有多少个子区间的异或值为\(k\)
    -\(1 \le n,m \le 10 ^ 5\)\(0 \le k,a_i \le 10^6\)\(1 \le l_i \le r_i \le n\)

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

分析

看到异或区间和,条件反射式想到异或前缀和,那么我们首先构造原序列的异或前缀和\(s\)。看本题,不难发现是个莫队,考虑如何快速实现增删一个端点

假设当前的需要增删的是区间端点是\(x\),那么就是求当前莫队的区间\([l,r]\)内有多少个数\(y\)满足x^y=k,可以推出y=k^x,此题就变成了一道莫队的查询某个值出现的次数,由于值域在\(10^6\)范围,所以可以开一个桶维护。

需要注意的是,异或前缀和查询区间异或和是需要左端点减一的,解决方案可以让所有的区间的左端点向左移动一位。

还有一些细节:在增加一个数进入区间的时候得先统计再插入,在删除的时候要先统计再删除

#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;
	}
}

莫队算法的变形

带修莫队

没想到吧,这玩意还能带修,但只能单点修改

带修莫队的原理是:给每一个询问加一个时间戳\(t\),表示在这个询问之前进行了\(t\)次修改,一个很暴力的思路是:

  1. 首先设计出此题若不考虑修改,普通莫队怎么做
  2. 对每一个查询打上时间戳\(t\)
  3. 正常莫队,在每一次莫队的调整\(l,r\)后,调整\(t\)这一维度,对于修改的位置直接暴力修改
  4. 需要注意,修改的值与原序列的值要交换,因为\(t\)可能会滚回来,改回来

需要注意的是,我们将\(t\)作为第三维度,对于右端点在同一个块内的按照\(t\)升序排序
光说不练假把式,上模版

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;
	}

来分析分析复杂度:

设块长为\(b\),我们有\(\frac{n^2}{b^2}\)个左端点的块的编号一致,右端点的块的编号一致组成的数对,又因为对于这样的数对中\(t\)是单调递增的,所以最多滚\(n\)次,故这部分算法复杂度是\(O(\frac{n^3}{b^2})\),然后因为左右端点的修改都会跳\(O(b)\)次,一共有\(O(n)\)组左右端点所属块不同的区间,总复杂度即为\(O(\frac{n^3}{b^2}+nb)\),由均值不等式,当\(\frac{n^3}{b^2}=nb\)时,复杂度最小,变式换算得到\(n^2=b^3\)也即\(b=\sqrt[\frac{2}{3}]{n}\)时最小,此时总复杂度为\(O(n^{\frac{5}{3}})\),总比暴力好。实际上由于常数较小的优点,部分\(10^5\)数据是能够跑过的

来两道例题:

P2464

题目描述

小 J 是国家图书馆的一位图书管理员,他的工作是管理一个巨大的书架。虽然他很能吃苦耐劳,但是由于这个书架十分巨大,所以他的工作效率总是很低,以致他面临着被解雇的危险,这也正是他所郁闷的。

具体说来,书架由\(N\)个书位组成,编号从\(1\)\(N\)。每个书位放着一本书,每本书有一个特定的编码。

小 J 的工作有两类:

  1. 图书馆经常购置新书,而书架任意时刻都是满的,所以只得将某位置的书拿掉并换成新购的书。

  2. 小 J 需要回答顾客的查询,顾客会询问某一段连续的书位中某一特定编码的书有多少本。

例如,共\(5\)个书位,开始时书位上的书编码为\(1, 2, 3, 4, 5\)

一位顾客询问书位\(1\)到书位\(3\)中编码为“\(2\)”的书共多少本,得到的回答为:\(1\)

一位顾客询问书位\(1\)到书位\(3\)中编码为“\(1\)”的书共多少本,得到的回答为:\(1\)

此时,图书馆购进一本编码为“\(1\)”的书,并将它放到\(2\)号书位。

一位顾客询问书位\(1\)到书位\(3\)中编码为“\(2\)”的书共多少本,得到的回答为:\(0\)

一位顾客询问书位\(1\)到书位\(3\)中编码为“\(1\)”的书共多少本,得到的回答为:\(2\)

……

你的任务是写一个程序来回答每个顾客的询问。

输入格式

第一行两个整数\(N, M\),表示一共\(N\)个书位,\(M\)个操作。

接下来一行共\(N\)个整数数\(A_1, A_2, \ldots , A_N\)\(A_i\)表示开始时位置\(i\)上的书的编码。

接下来\(M\)行,每行表示一次操作,每行开头一个字符。

若字符为 C,表示图书馆购进新书,后接两个整数\(A, P\)\(1 \le A \le N\)),表示这本书被放在位置\(A\)上,以及这本书的编码为\(P\)

若字符为 Q,表示一个顾客的查询,后接三个整数\(A, B, K\)\(1 \le A \le B \le N\)),表示查询从第\(A\)书位到第\(B\)书位(包含\(A\)\(B\))中编码为\(K\)的书共多少本。

输出格式

对每一个顾客的查询,输出一个整数,表示顾客所要查询的结果。

样例 #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

提示

对于\(40 \%\)的数据,\(1 \le N, M \le 5000\)

对于\(100 \%\)的数据,\(1 \le N, M \le {10}^5\)

对于\(100 \%\)的数据,所有出现的书的编码为不大于\(2^{31} - 1\)的正数。

分析

一句话题意:给定一段区间,支持单点修改和区间定值出现数量查询

套路:离线,离散化,开一个计数数组计数,然后就变成了板子。。。

例2

题意:给定一个序列,有两种操作
1 x y表示将位置为x的数改为y
2 l r表示查询区间\([l,r]\)中出现过的一次的数的个数.
提示:值域与\(n\)同级

分析

很明显开个桶记录每个数出现次数,那么对于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

题面翻译

给你一个数组\(\{a_i\}\)支持两种操作:

1、查询区间\([l,r]\)中每个数字出现次数的mex。

2、单点修改某一个位置的值。

mex指的是一些数字中最小的未出现的自然数。值得注意的是,区间\([l,r]\)总有数字是没有出现过的,所以答案不可能为0.

\(n,Q\le10^5\)

感谢@租酥雨 提供的翻译

题目描述

You come home and fell some unpleasant smell. Where is it coming from?

You are given an array\(a\). You have to answer the following queries:

  1. You are given two integers\(l\)and\(r\). Let\(c_{i}\)be the number of occurrences of\(i\)in\(a_{l:r}\), where\(a_{l:r}\)is the subarray of\(a\)from\(l\)-th element to\(r\)-th inclusive. Find the Mex of\({c_{0},c_{1},...,c_{10^{9}}}\)
  2. You are given two integers\(p\)to\(x\). Change\(a_{p}\)to\(x\).

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\(a\)are positive, which means that\(c_{0}\)= 0 and\(0\)is never the answer for the query of the second type.

输入格式

The first line of input contains two integers\(n\)and\(q\)(\(1<=n,q<=100000\)) — the length of the array and the number of queries respectively.

The second line of input contains\(n\)integers —\(a_{1}\),\(a_{2}\),\(...\),\(a_{n}\)(\(1<=a_{i}<=10^{9}\)).

Each of the next\(q\)lines describes a single query.

The first type of query is described by three integers\(t_{i}=1\),\(l_{i}\),\(r_{i}\), where\(1<=l_{i}<=r_{i}<=n\)— the bounds of the subarray.

The second type of query is described by three integers\(t_{i}=2\),\(p_{i}\),\(x_{i}\), where\(1<=p_{i}<=n\)is the index of the element, which must be changed and\(1<=x_{i}<=10^{9}\)is the new value.

输出格式

For each query of the first type output a single integer — the Mex of\({c_{0},c_{1},...,c_{10^{9}}}\).

样例 #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 —\(1\).

The subarray of the second query consists of four\(2\)s, one\(3\)and two\(1\)s.

The subarray of the fourth query consists of three\(1\)s, three\(2\)s and one\(3\).

分析

感觉一般数据结构根本无法维护,考虑莫队。

先分析如果没有修改怎么办,即adddel如何设计

很有用的性质:每个数adddel的时候,最多使得这个数出现次数改变\(1\),考虑这个1如何维护。因为它维护的是区间出现次数,不妨考虑先离散化,再开桶来维护每个数出现的次数,记为\(cnt1\),再开一个桶维护每个出现次数一共有几个,记为\(cnt2\),这样我们可以保证\(O(1)\)统计出来次数,考虑如何求解mex,设当前统计出的mex\(sum\),首先答案不可能为0,当我们需要更改\(sum\)的时候,也仅仅会改变两个值,记为\(x,y\),先来讨论add
假设我们加入了数\(s\),那么就需要

cnt2[cnt1[s]]--;//此时为0可能会更新mex
cnt1[s]++;
cnt2[cnt1[s]]++;

考虑其对mex的影响,当第一次减去\(cnt2\)时,若其为0则可能会更新\(mex\),取\(\min\)即可,问题在于若原本\(mex\)为后面增加的那个\(cnt2\),考虑如何维护这个更改,此时肯定是往后找到第一个为0的\(cnt2\),但好似这个过程没有什么可以优化的,还不如区间移动到好了直接暴力mex,但直接暴力求解mex复杂度如何呢,让我们想想,从1开始扫,最坏情况下,即为有一个数出现1次,一个数出现2次,一个数出现3次……一直到\(x\),那么,可以发现,总得数的数量为:\(\sum_{i=1}^xi\le n\),所以说这个暴力求解的复杂度是远小于单次\(O(\sqrt n)\)的,故总的复杂度也就\(O(n\sqrt n)\),那么莫队的复杂度大于它,故不会影响复杂度

于是我们就得到了一个看似无比暴力的莫队算法,add,del完全只需要维护cnt1,cnt2即可,无需考虑影响,最后当\(l,r,t\)三维归一,则暴力扫描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]);
}

回滚莫队

由于莫队的核心操作是\(add,del\)两个,整个莫队的复杂度基于这两个函数的算法复杂度乘上\(O(N\sqrt N)\),但很多时候,莫队算法的这两个函数其中一个仍然可以保证\(O(1)\),但另一个函数的复杂度较高,比如\(\log n,\sqrt n\)之类的,此时普通的莫队算法就不能解决问题。我们就有了回滚莫队的算法,对于\(add,del\)哪一个复杂度较高,又可以划分为两类:只删不增的回滚莫队,只增不删的回滚莫队其核心思想是通过不断记录标记,将指针拉回,化增加/删除为撤销(即不改变答案,只\(O(1)\)更改维护的信息)

只删不增的回滚莫队

这里主要是先暴力统计大区间答案,再通过不断删这个大区间达到只删不增的效果

  1. 首先将询问的区间以左端点所在块为第一关键字升序,以右端点为第二关键字降序排序
  2. 对于左右端点都在同一个块的询问,直接暴力统计答案,复杂度为\(O(\sqrt N)\)
  3. 对于左端点在同一个块的询问,将\(l\)初始化为这个块的左端点,将\(r\)初始化为\(n\),这部分先暴力统计这段答案,复杂度一般\(O(N)\)
  4. 由于右端点降序,在处理这同一个块的询问的时候,右端点单调递减,只用删除
  5. 由于左端点可能无序,考虑建立\(tmp\)先记录下左端点为块的左端点的答案,然后将左端点向右移动,同样是只用删除,当到达指定位置之后,统计一次答案,然后撤销删除,但不统计答案,当撤销回块左端点时,再将答案重新变为\(tmp\)

在实现上,为了代码的方便,可以给左右端点在同一个块的区间单独开一组统计数组来暴力求解

//只删不增回滚莫队伪代码
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;
	}
}

不难得知,复杂度为\(O(N\sqrt N)\)

例题:Rmq Problem / mex

描述

有一个长度为 \(n\) 的数组 \(\{a_1,a_2,\ldots,a_n\}\)

\(m\) 次询问,每次询问一个区间内最小没有出现过的自然数。

输入

第一行,两个正整数 \(n,m\)
第二行,\(n\) 个非负整数 \(a_1, a_2, \ldots , a_n\)
接下来 \(m\) 行,每行两个正整数 \(l,r\),表示一次询问。

分析

首先按照套路维护计数数组,然后考虑删除怎么做,很明显,令这个数出现次数减一,减到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;
}

只增不删的回滚莫队

类比只删不增的回滚莫队,容易想到可以这样做:

  1. 将询问区间按左端点所在块为第一关键字递增排序,右端点为第二关键字递增排序
  2. 对于每个块,初始化\(l=R[i]+1,r=l-1\),这是一个空区间
  3. 对于左右端点在同一个块内的询问,暴力处理即可
  4. 否则,对于右端点的处理,由于递增,所以只增不删
  5. 对于左端点,先记录下此时的答案,然后只增不删往左滚,更新答案之后原路撤销操作
  6. 将答案还原即可
    这里没有处理新开的块,原因是本身是一个空区间
    板子大概类似
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++);
	}
}

例题:洛谷模版

给定一个序列,多次询问一段区间 \([l,r]\),求区间中相同的数的最远间隔距离。

序列中两个元素的间隔距离指的是两个元素下标差的绝对值。

分析

解法1

考虑回滚莫队的常规操作,由于要求相同元素距离最大值,显然必须对于每一个值都求出相邻元素最大值,而欲求这个最大值,就必须知道当前区间每个值最后出现的位置和最先出现的位置,分别记为\(ed,st\),考虑如何维护这两个值。由于我们每处理完一个块的询问之后会重置,无需考虑不同块的相互影响,那么仅需考虑一个块如何维护即可。由于回滚莫队的性质,显然\(r\)单调递增,\(ed\)\(st\)就可以肆无忌惮的在\(r\)中更新,很简单不多说。而至于要回滚的\(l\)指针,则不能维护\(st\),因为无法撤销,但幸好,当前\(l\)指针所在位置,就是这个位置对应值的\(st\),故我们只需要对只出现于\(l\)移动范围的值更新\(ed\)即可。那么最后回滚的时候,如果发现回滚的\(l\)指针这个位置的\(ed\)指向自己,就重置为0。在每个块询问处理完之后,不要忘记清空两数组

同块暴力很简单,不多说

#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

有一说一,此题不用回滚莫队,可以\(O(n)\)的预处理出每个数出现的位置的前一个和后一个出现位置,那么将\(add,del\)都分\(l,r\)讨论,借助前驱后继即可进行更新

树上莫队

二次离线莫队

posted @ 2022-11-30 22:41  spdarkle  阅读(46)  评论(0编辑  收藏  举报