莫队

关于莫队算法

普通莫队

带修莫队

树上莫队

回滚莫队

大致分为这4类莫队算法


莫队算法——优雅而不失复杂度的暴力

莫队算法是一种像暴力一样好写,同时具有分块算法时间复杂度的技巧。其特点是常数较小,适合离线处理问题。

如果从 \([l, r]\) 的答案能够在 \(O(1)\) 时间内扩展到 \([l+1, r]\)\([l-1, r]\)\([l, r+1]\)\([l, r-1]\)(即与 \([l, r]\) 相邻的区间)的答案,那么使用莫队算法可以在 \(O(n\sqrt{n})\) 的复杂度内求出所有询问的答案。


莫队算法基本原理

双指针跳跃

如下图所示,假如我们要求 \(Q_2\) 的区间和,并且已经得到了 \(Q_1\) 的区间和:

图示说明双指针跳跃原理
  1. \(Q_1\) 的右指针右移,沿路累加经过的数值,得到区间 \([1, 15]\) 的区间和。
  2. 然后将 \(Q_1\) 的左指针右移,沿路减去经过的数值,得到区间 \([6, 15]\) 的区间和。

同理,从 \(Q_2\)\(Q_1\) 也是如此(右移变为左移)。

我们称操作 1 为加入操作,操作 2 为删除操作。
在莫队算法中,加入删除操作是解决问题的核心。


具体实现

定义状态

假设我们当前在区间 \([l, r]\)
- 用 \(vis[i]\) 表示当前区间中数字 \(i\) 出现的次数。
- 如果某个 \(i\)\(vis[i]\) 超过 1,则当前区间不满足互不相同的条件。
- 用一个计数器 \(sum\) 表示当前区间中 \(vis[i] > 1\) 的数字的个数。
- 如果 \(sum \neq 0\),表示当前区间不满足互不相同的条件。

加入操作

每加入一个数 \(i\)
1. \(vis[i] += 1\)
2. 如果 \(vis[i]\) 从 1 变为超过 1,则 \(sum += 1\)

删除操作

每删除一个数 \(i\)
1. \(vis[i] -= 1\)
2. 如果 \(vis[i]\) 从超过 1 变为 1,则 \(sum -= 1\)

判断结果

在移动到指定区间后:
- 如果 \(sum > 0\),输出 No
- 如果 \(sum = 0\),输出 Yes

1
2
void add(int x){if(vis[v[x]]==1)ans++;vis[v[x]]++;}	//加入操作
void remove(int x){vis[v[x]]--;if(vis[v[x]]==1)ans--;}//删除操作
1
2
3
4
5
6
7
8
9
10
l=1;r=0;
for(int i=1;i<=m;i++){
while(l>a[i].l)add(--l);//左指针往左移加入
while(l<a[i].l)remove(l++);//左指针往右移删除
while(r<a[i].r)add(++r);//右指针往右移加入
while(r>a[i].r)remove(r--);//右指针往左移删除
if(ans==0)a[i].ans=0; //Yes
else a[i].ans=1; //No
}

莫队中要注意的细节——指针移动

首先可以明确一点:

左指针向左移是加入操作,向右移是删除操作

右指针向左移是删除操作,向右移是加入操作

加入操作是移动指针,将指向的值加入

而删除是将指针所指的值删除,将指针移动

刚开始的区间是[1,0],可以发现很奇怪啊

不应该l<=r吗,怎么l>r

我们可以用前缀和的思想想一下

l代表什么?代表[0,l)的位置都删除过了

r代表什么?代表[0,r]的位置都加入过了

那么代入l=1,r=0正好就是区间[0,0],也就是什么都没有。

那就可以解释得通了!

明显,如果我们按询问区间这样暴力去跳左右指针,是可以被卡的

复杂度\(O(n\sqrt{n}\))

算法流程:

我们将长度为n的序列分成O(\(\sqrt{n}\))块,从1至\(\sqrt{n}\)编号。

然后标记每一个点所属的块block[i](表示i点属于块的编号)

然后我们对询问排序:

先按左端点所在的块编号为第一关键字排序

如果相同,则按右端点的位置排序

然后按排序后的顺序处理每一个询问,求得答案

再通过一次排序复原原来的顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bool cmp1(kkk a,kkk b){return a.id<b.id;}		
//复原原来顺序
bool cmp(kkk a,kkk b){
//询问排序
if(blo[a.l]!=blo[b.l])return a.l<b.l;
//先按左端点所在块为第一关键字排序
else return a.r<b.r;
//相同则按右端点排序
}
block=sqrt(n); //块大小
for(int i=1;i<=n;i++)
blo[i]=(i-1)/block+1;
//划分块
for(int i=1;i<=m;i++)
{
cin>>a[i].l>>a[i].r;
a[i].id=i;
//标记原来编号
}
sort(a+1,a+m+1,cmp);
//对询问排序
sort(a+1,a+m+1,cmp1);
//复原顺序

莫队的优化

对于左端点在同一奇数(编号)块时,右端点按升序排序,在同一偶数(编号)块时,右端点按降序排序。

1
2
3
4
5
6
bool cmp(kkk a,kkk b)
{
if(blo[a.l]!=blo[b.l])return a.l<b.l;
if(blo[a.l]%2==0)return a.r>b.r;//多加了行判断
return a.r<b.r;
}

据数据结构神lxl大佬说,普通莫队将块大小调节成\(n/\sqrt{m}\) 会块。

1
block=n/sqrt(m);

[国家集训队] 小 Z 的袜子

题目描述

upd on 2020.6.10 :更新了时限。

作为一个生活散漫的人,小 Z 每天早上都要耗费很久从一堆五颜六色的袜子中找出一双来穿。终于有一天,小 Z 再也无法忍受这恼人的找袜子过程,于是他决定听天由命……

具体来说,小 Z 把这 \(N\) 只袜子从 \(1\)\(N\) 编号,然后从编号 \(L\)\(R\) 的袜子中随机选出两只来穿。尽管小 Z 并不在意两只袜子是不是完整的一双,他却很在意袜子的颜色,毕竟穿两只不同色的袜子会很尴尬。

你的任务便是告诉小 Z,他有多大的概率抽到两只颜色相同的袜子。当然,小 Z 希望这个概率尽量高,所以他可能会询问多个 \((L,R)\) 以方便自己选择。

然而数据中有 \(L=R\) 的情况,请特判这种情况,输出0/1

输入格式

输入文件第一行包含两个正整数 \(N\)\(M\)\(N\) 为袜子的数量,\(M\) 为小 Z 所提的询问的数量。接下来一行包含 \(N\) 个正整数 \(C_i\),其中 \(C_i\) 表示第 \(i\) 只袜子的颜色,相同的颜色用相同的数字表示。再接下来 \(M\) 行,每行两个正整数 \(L, R\) 表示一个询问。

输出格式

包含 \(M\) 行,对于每个询问在一行中输出分数 \(A/B\) 表示从该询问的区间 \([L,R]\) 中随机抽出两只袜子颜色相同的概率。若该概率为 \(0\) 则输出 0/1,否则输出的 \(A/B\) 必须为最简分数。(详见样例)

样例 #1

样例输入 #1

1
2
3
4
5
6
6 4
1 2 3 3 3 2
2 6
1 3
3 5
1 6

样例输出 #1

1
2
3
4
2/5
0/1
1/1
4/15

提示

\(30\%\) 的数据中,\(N,M\leq 5000\)

\(60\%\) 的数据中,\(N,M \leq 25000\)

\(100\%\) 的数据中,\(N,M \leq 50000\)\(1 \leq L < R \leq N\)\(C_i \leq N\)

假设此时有x对颜色为a的袜子,那么取到一对a的方案数是a(a−1) 很显然吧

那么加入1只颜色为a的袜子,我们把原来取一对颜色a的方案数减掉,然后更新a的数量

再把取一对颜色a的方案数加回去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include<bits/stdc++.h>
#define maxn 100001
using namespace std;
struct kkk
{
long long l,r,id,a,b;
}a[maxn];
long long ans,ansl[maxn],ansr[maxn],gcd1;
int blo[maxn],block,n,m,l,r,tag[maxn],v[maxn];
bool cmp1(kkk a,kkk b)
{return a.id<b.id;}
bool cmp(kkk a,kkk b)
{
if(blo[a.l]!=blo[b.l])return a.l<b.l;
if(blo[a.l]%2==0)return a.r>b.r;//多加了行判断
return a.r<b.r;
}
void add(int x){ans-=tag[v[x]]*(tag[v[x]]-1);tag[v[x]]++;ans+=tag[v[x]]*(tag[v[x]]-1);}
void remove(int x){ans-=tag[v[x]]*(tag[v[x]]-1);tag[v[x]]--;ans+=tag[v[x]]*(tag[v[x]]-1);}
int main()
{
cin>>n>>m;
block=n/sqrt(m);
for(int i=1;i<=n;i++)cin>>v[i];
for(int i=1;i<=n;i++)blo[i]=(i-1)/block+1;
for(int i=1;i<=m;i++){
cin>>a[i].l>>a[i].r;
a[i].id=i;
}
sort(a+1,a+m+1,cmp);
l=1;r=0;
for(int i=1;i<=m;i++){
while(l>a[i].l)add(--l);
while(l<a[i].l)remove(l++);
while(r<a[i].r)add(++r);
while(r>a[i].r)remove(r--);
if(a[i].l==a[i].r)
{
ansl[i]=0;ansr[i]=1;
a[i].a=ansl[i];a[i].b=ansr[i];
continue;
}
ansl[i]=ans;
ansr[i]=(a[i].r-a[i].l+1)*(a[i].r-a[i].l);
gcd1=__gcd(ansl[i],ansr[i]);
ansl[i]/=gcd1;ansr[i]/=gcd1;
a[i].a=ansl[i];a[i].b=ansr[i];
}
sort(a+1,a+m+1,cmp1);
for(int i=1;i<=m;i++)
cout<<a[i].a<<'/'<<a[i].b<<'\n';
}

小B的询问

题目描述

小B 有一个长为 \(n\) 的整数序列 \(a\),值域为 \([1,k]\)
他一共有 \(m\) 个询问,每个询问给定一个区间 \([l,r]\),求:
\[\sum\limits_{i=1}^k c_i^2\]

其中 \(c_i\) 表示数字 \(i\)\([l,r]\) 中的出现次数。
小B请你帮助他回答询问。

输入格式

第一行三个整数 \(n,m,k\)

第二行 \(n\) 个整数,表示 小B 的序列。

接下来的 \(m\) 行,每行两个整数 \(l,r\)

输出格式

输出 \(m\) 行,每行一个整数,对应一个询问的答案。

样例 #1

样例输入 #1

1
2
3
4
5
6
6 4 3
1 3 2 1 1 3
1 4
2 6
3 5
5 6

样例输出 #1

1
2
3
4
6
9
5
2

提示

【数据范围】
对于 \(100\%\) 的数据,\(1\le n,m,k \le 5\times 10^4\)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <bits/stdc++.h>
using namespace std;
const int N=5e4+10;
int cnt[N],a[N],ans=0;
struct Que{
int l,r,id;//查询结构题
}que[N];
int T,Ans[N];
bool cmp(Que a,Que b){
if(a.l/T!=b.l/T) return a.l/T<b.l/T;//第一个关键字为左端点所在块编号
return a.r<b.r;//第二个关键字为右端点编号
}
void add(int x){
ans-=cnt[x]*cnt[x];
cnt[x]++;
ans+=cnt[x]*cnt[x];
}
void del(int x){
ans-=cnt[x]*cnt[x];
cnt[x]--;
ans+=cnt[x]*cnt[x];
}
int n,m,k;
int main(){
cin>>n>>m>>k;
T=sqrt(n);
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=m;i++) cin>>que[i].l>>que[i].r,que[i].id=i;//离线读入询问
sort(que+1,que+1+m,cmp);//先排序
int l=1,r=0;//起始lr指针设为一个空区间
for(int i=1;i<=m;i++){
//莫队算法
while(r<que[i].r) add(a[++r]);
while(r>que[i].r) del(a[r--]);
while(l>que[i].l) add(a[--l]);
while(l<que[i].l) del(a[l++]);
Ans[que[i].id]=ans;//记录答案
}
for(int i=1;i<=m;i++) cout<<Ans[i]<<endl;
}

带修莫队

[国家集训队] 数颜色 / 维护队列

题目描述

墨墨购买了一套 \(N\) 支彩色画笔(其中有些颜色可能相同),摆成一排,你需要回答墨墨的提问。墨墨会向你发布如下指令:

  1. \(Q\ L\ R\) 代表询问你从第 \(L\) 支画笔到第 \(R\) 支画笔中共有几种不同颜色的画笔。

  2. \(R\ P\ C\) 把第 \(P\) 支画笔替换为颜色 \(C\)

为了满足墨墨的要求,你知道你需要干什么了吗?

输入格式

\(1\) 行两个整数 \(N\)\(M\),分别代表初始画笔的数量以及墨墨会做的事情的个数。

\(2\)\(N\) 个整数,分别代表初始画笔排中第 \(i\) 支画笔的颜色。

\(3\) 行到第 \(2+M\) 行,每行分别代表墨墨会做的一件事情,格式见题干部分。

输出格式

对于每一个 Query 的询问,你需要在对应的行中给出一个数字,代表第 \(L\) 支画笔到第 \(R\) 支画笔中共有几种不同颜色的画笔。

样例 #1

样例输入 #1

1
2
3
4
5
6
7
6 5
1 2 3 4 5 5
Q 1 4
Q 2 6
R 1 2
Q 1 4
Q 2 6

样例输出 #1

1
2
3
4
4
4
3
4

提示

对于30%的数据,\(n,m \leq 10000\)

对于60%的数据,\(n,m \leq 50000\)

对于所有数据,\(n,m \leq 133333\)

所有的输入数据中出现的所有整数均大于等于 \(1\) 且不超过 \(10^6\)

本题可能轻微卡常数

来源:bzoj2120

本题数据为洛谷自造数据,使用 CYaRon 耗时5分钟完成数据制作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int T;
struct Que
{
int l,r,id,tim;
}que[N];
int as[N];
bool cmp(Que a,Que b)
{
if(a.l/T==b.l/T)
{
if(a.r/T==b.r/T) return a.tim<b.tim;
return a.r/T<b.r/T;
}
return a.l/T<b.l/T;
}
struct Upd{
int p,col;
}upd[N];
int cnt[N],ans=0,a[N];
void add(int x)
{
if(!cnt[x]) ans++;
cnt[x]++;
}
void del(int x)
{
cnt[x]--;
if(!cnt[x]) ans--;
}
int n,m,iq=0,ic=0;
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=m;i++)
{
char opt;
int l,r;
cin>>opt>>l>>r;
if(opt=='Q') que[++iq]={l,r,iq,ic};
else upd[++ic]={l,r};
}
T=pow(n,0.666);//即n^2/3
sort(que+1,que+1+iq,cmp);//排序
int l=1,r=0,now=0;//三个指针
for(int i=1;i<=m;i++)
{
//这部分和普通莫队差不多
while(l<que[i].l) del(a[l++]);
while(l>que[i].l) add(a[--l]);
while(r>que[i].r) del(a[r--]);
while(r<que[i].r) add(a[++r]);
//转移到新询问的时间戳
while(now<que[i].tim)
{
++now;
int p=upd[now].p,c=upd[now].col;
if(l<=p&&p<=r) del(a[p]),add(c);
swap(a[p],upd[now].col);
}
while(now>que[i].tim){
int p=upd[now].p,c=upd[now].col;
if(l<=p&&p<=r) del(a[p]),add(c);
swap(a[p],upd[now].col);
now--;
}
as[que[i].id]=ans;//记录答案
}
for(int i=1;i<=iq;i++) cout<<as[i]<<endl;
}
120b5177770240188bf5800b1058c64