写点什么

秒懂算法 | 莫队算法

作者:TiAmo
  • 2023-02-22
    江苏
  • 本文字数:6444 字

    阅读完需:约 21 分钟

秒懂算法 | 莫队算法

01、基础莫队算法

莫队算法 = 离线 + 暴力 + 分块。

“离线”和“在线”的概念。在线是交互式的,一问一答;如果前面的答案用于后面的提问,称为“强制在线”。离线是非交互的,一次性读取所有问题,然后一起回答,"记录所有步,回头再做”。

基础的莫队算法是一种离线算法,它通常用于不修改只查询的一类区间问题,复杂度 O(n√n),没有在线算法线段树或树状数组好,但是编码很简单。下面是一道莫队模板题。

HH 项链 洛谷 1972

题目描述:给定一个数量,询问某个区间内不同的数有多少个。

输入:第一行一个正整数 n,表示数列长度。第二行 n 个正整数 ai。第三行一个整数 m,表示 HH 询问的个数。接下来 m 行,每行两个整数 L,R,表示询问的区间。

输出:输出 m 行,每行一个整数,依次表示询问对应的答案。

题目询问区间内不同的数有多少个,即去重后数字的个数,本题的标准解法是线段树或树状数组。下面首先给出暴力法,然后再引导出莫队算法。

1. 暴力法

可以用 STL 的 unique()函数去重,一次耗时 O(n),m 次的总复杂度 O(mn)。或者自己编码, 用扫描法统计数字出现的次数,这是一种简单易行的暴力法。

(1)查询一个区间有多少个不同的数字

定义 cnt[],cnt[x]表示数字 x 出现的次数;定义答案为 ans,即区间内不同的 x 有多少个。

用指针 L、R 单向扫描,从数列的头扫到尾,L 最终落在查询区间的最左端,R 落在区间最右端。L 往右每扫一个数 x,就把它出现的次数 cnt[x]减去 1;R 往右每扫到一个数 x,就把它出现的次数 cnt[x]加上 1。扫描完区间后,cnt[x]的值就是 x 在区间内出现的次数。若 cnt[x]=1 说明 x 第 1 次出现,ans 加 1;若 cnt[x]变为 0,说明它从区间里消失了,ans 减 1。

下面的例子是统计区间[3, 7]内有多少不同的数字,初始指针 L=1,R=0。


▍图 1 统计一个区间

图(1):L=1、R=0 时,cnt[4]=0, cnt[7]=0, cnt[9]=0…答案 ans=0。

图(2):L=2、R=0 时,cnt[4]=-1, cnt[7]=0。

图(3):L=3、R=2 时,cnt[4]=0, cnt[7]=0, cnt[9]=0…

图(4):L=3、R=3 时,cnt[4]=0, cnt[7]=0, cnt[9]=1。出现了一个等于 1 的 cnt[9],答案 ans = 1。

图(5):L=3、R=7 时,cnt[4]=1, cnt[7]=0, cnt[9]=1, cnt[6]=2, cnt[3]=1,…其中 cnt[4], cnt[9], cnt[6], cnt[3]都出现过等于 1 的情况,所以答案 ans = 4。

(2)统计多个区间

从上面查询一个区间的讨论可以知道,在 L、R 移动过程中,当它们停留在区间[L, R]时,就得到了这个区间的答案 ans。那么对 m 个询问,只要不断移动 L、R 并与每个询问的区间匹配,就得到了 m 个区间询问的答案。

为了方便操作,可以把所有询问的区间按左端点排序;如果左端点相同,再按右端点排序。讨论以下情况:

1)简单情况,区间交错,区间[x1, y1]、[x2, y2]的关系是 x1 ≤ x2,y1 ≤ y2。例如下图中,查询两个区间[2, 6]、[4, 8]。


▍图 2 简单情况

图(1)L、R 停留在第 1 个区间上,得到了第 1 个区间的统计结果;图(2)L、R 停留在第 2 个区间上,得到了第 2 个区间的结果。m 次查询的 m 个区间,L、R 指针只需要从左头到右(单向移动)扫描整个数组一次即可,总复杂度 O(n)。

2)复杂情况,既有区间交错,又有区间包含。区间[x1, y1]、[x2, y2]的包含关系是指 x1 ≤ x2,y1 ≥ y2。例如下图中,区间[2, 9]包含了区间[3, 5]。此时 L 从头到尾单向扫描,而 R 指针却需要来回往复扫描,每次扫描的复杂度是 O(n)。m 次查询的总复杂度是 O(mn)。


▍图 3 复杂情况

R 往复移动的时候,R 往左每扫一个数 x,就把它出现的次数 cnt[x]减去 1。

2. 区间查询问题的几何解释

洛谷 P1972 的区间查询问题,可以概况为这样一种离线的几何模型:

(1)m 个询问对应 m 个区间,区间之间的转移,可以用 L、R 指针扫描,能以 O(1)的复杂度从区间[L,R]移动到[L±1, R±1]。

(2)把一个区间[L, R]看成平面上的一个坐标点(x, y),L 对应 x,R 对应 y,那么区间的转移等同于平面上坐标点的转移,计算量等于坐标点之间的曼哈顿距离。注意,所有的坐标点(x, y)都满足 x ≤ y,即所有的点都分布在上半平面上。

(3)完成 m 个询问,等于从原点出发,用直线连接这 m 个点,形成一条“Hamilton 路径”,路径的长度就是计算量。若能找到一条最短的路径,计算量就最少。

Hamilton 最短路径问题是 NP 难度的,没有多项式复杂度的解法。那么有没有一种较优的算法,能快速得到较好的结果呢?

暴力法是按照坐标点(x, y)的 x 值排序而生成的一条路径,它不是好算法。例如下图(1)的简单情况,暴力法的顺序是好的;但是图(2)的复杂情况,暴力法的路径是(0, 0)-(2, 9)-(3, 5),曼哈顿距离(2-0) + (9-0) + (3-2) + (9-5) = 16,不如另一条路径(0, 0)-(3, 5)-(2, 9),曼哈顿距离 = 13。


▍图 4 暴力法的路径

下面介绍的莫队算法,提出了一种较好的排序方法。

3. 莫队算法

莫队算法把排序做了简单的修改,就把暴力法的复杂度从 O(mn)提高到 O(n√n)。

(1)暴力法的排序:把查询的区间按左端点排序,如果左端点相同,再按右端点排序。

(2)莫队算法的排序:把数组分块(分成√n 块),然后把查询的区间按左端点所在块的序号排序,如果左端点的块相同,再按右端点排序。

除了排序不一样,莫队算法和暴力法的其他步骤完全一样。

这个简单的修改是否真能提高效率?下面分析多种情况下莫队算法的复杂度。

(1)简单情况。区间交错,设区间[P1,y1]、[P2,y2]的关系是 P1<P2,y1≤y2,其中 P1、P2 是左端点所在的块。L、R 只需要从左到右扫描一次,m 次查询的总复杂度是 O(n)。

(2)复杂情况。区间包含,设两个区间查询[P1,y1]、[P2,y3]的关系是 P1=P2,y2≤y1。如下图所示。


▍图 5 按块排序后的区间包含

此时小区间[P2,y2]排在大区间 P1,y1]的前面,与暴力法正好相反。在区间内,右指针 R 从左到右单向移动,不再往复移动。而左指针 L 发生了回退移动,但是被限制在一个长为 的块内,每次移动的复杂度是 O(√n)的。m 次查询,每次查询左端点只需要移动 O(√n)次,右端点 R 共单向移动 O(n)次,总复杂度 O(√n)。

(3)特殊情况:m 个询问,端点都在不同的块上,此时莫队算法和暴力法是一样的。但此时 m 小于 ,总复杂度 O(mn) = O(n√n)。

4. 莫队算法的几何解释

莫队算法的几何意义见图 6(感谢莫涛对此图提出修改意见),这张图透彻说明了莫队算法的原理。图中的每个黑点是一个查询。

图 6(1)是暴力法排序后的路径,所有的点按 x 坐标排序,在复杂情况下,路径沿 y 方向来回往复,震荡幅度可能非常大(纵向震荡,幅度 O(n)),导致路径很长。

图 6(2)是莫队算法排序后的路径,它把 x 轴分成多个区(分块),每个区内的点按 y 坐标排序,在区内沿 x 方向来回往复,此时震荡幅度被限制在区内(横向震荡,幅度 O(√n)),形成了一条比较短的路径,从而实现了较好的复杂度。


▍图 6 暴力法和莫队算法的几何对比

通过图 6(2)可以更清晰地计算莫队算法的复杂度:

(1)x 方向的复杂度。在一个区块内,沿着 x 方向一次移动最多√n,所有区块共有 m 次移动,总复杂度 O(m√n)。

(2)y 方向的复杂度。在每个区块内,沿着 y 方向单向移动,整个区块的 y 方向长度是 n,有√n 个区块,总复杂度 O(n√n)。

两者相加,总复杂度 O(m√n+n√n),一般情况下题目会给出 n = m。

根据图 6 总结出莫队算法的核心思想:把暴力法的 y 方向的 O(n) 幅度的震荡,改为 x 方向的受限于区间的 O(√n)幅度的震荡,从而减少了路径的长度,提高了效率。

前面曾提到排序问题,对区间排序是先按左端点所在块排序,再按右端点排序,不是按右端点所在的块排序。原因解释如下:如果右端点也按块排序,几何图就需要画成一个方格图,方格中的点无法排序,实际的结果就是乱序。那么同一个方格内的点,在 y 方向上就不再是一直往上的复杂度为 O(n)的单向移动,而是忽上忽下的往复移动,导致路径更长,复杂度变差。见图 7 所演示的路径。


▍图 7 右端点按块排序是错误的

编码时,还可以对排序做一个小优化:奇偶性排序,让奇数块和偶数块的排序相反。例如左端点 L 都在奇数块,则对 R 从大到小排序;若 L 在偶数块,则对 R 从小到大排序(反过来也可以:奇数块从小到大,偶数块从大到小)。

这个小优化对照图 6(2)很容易理解,图中路径在两个区块之间移动时,是从左边区块的最大 y 值点移动到右边区块的最小 y 值点,跨度很大。用奇偶性排序后,奇数块的点从最大 y 值到最小 y 值点排序,偶数块从最小 y 值点到最大 y 值点排序;那么奇数块最后遍历的点是最小 y 值点,然后右移到偶数块的最小 y 值点,这样移动的距离是最小的。从偶数块右移到奇数块的情况类似。

下面是洛谷 P1972 的代码。莫队算法和暴力法唯一不同的地方在比较函数 cmp()中。

#include<bits/stdc++.h>using namespace std;const int maxn = 1e6;struct node{         //离线记录查询操作  int L, R, k; //k:查询操作的原始顺序}q[maxn];int pos[maxn];int ans[maxn];int cnt[maxn]; //cnt[i]: 统计数字i出现了多少次int a[maxn];bool cmp(node a, node b){  //按块排序,就是莫队算法:  if(pos[a.L] != pos[b.L]) //按L所在的块排序,如果块相等,再按R排序    return pos[a.L] < pos[b.L];  if(pos[a.L] & 1) return a.R > b.R; //奇偶性优化,如果删除这一句,性能差一点  return a.R < b.R;    /*如果不按块排序,而是直接L、R排序,就是普通暴力法:    if(a.L==b.L) return a.R < b.R;      return a.L < b.L; */}int ANS = 0;void add(int x){ //扩大区间时(L左移或R右移),增加数x出现的次数    cnt[a[x]]++;    if(cnt[a[x]]==1) ANS++; //这个元素第1次出现}void del(int x){ //缩小区间时(L右移或R左移),减少数x出现的次数    cnt[a[x]]--;    if(cnt[a[x]]==0) ANS--; //这个元素消失了}int main(){    int n; scanf("%d",&n);    int block = sqrt(n); //每块的大小    for(int i=1;i<=n;i++){        scanf("%d",&a[i]); //读第i个元素    pos[i]=(i-1)/block + 1; //第i个元素所在的块    }    int m; scanf("%d",&m);    for(int i=1;i<=m;i++){ //读取所有m个查询,离线处理        scanf("%d%d",&q[i].L, &q[i].R);        q[i].k = i; //记录查询的原始顺序    }    sort(q+1, q+1+m, cmp); //对所有查询排序  int L=1, R=0; //左右指针的初始值。思考为什么?    for(int i=1;i<=m;i++){         while(L<q[i].L) del(L++); //{del(L); L++;} //缩小区间:L右移        while(R>q[i].R) del(R--); //{del(R); R--;} //缩小区间:R左移    while(L>q[i].L) add(--L); //{L--; add(L);} //扩大区间:L左移        while(R<q[i].R) add(++R); //{R++; add(R);} //扩大区间:R右移        ans[q[i].k] = ANS;    }    for(int i=1;i<=m;i++) printf("%d\n",ans[i]); //按原顺序打印结果    return 0;}
复制代码

02、带修改的莫队

上一节的基础莫队算法只用于无修改只查询的区间问题,如果是比较简单的“单点修改”,也能应用莫队算法,得到复杂度 O(mn2/3)的算法。

下面的例题是“单点修改 + 区间询问”。

数颜色 洛谷 P1903

题目描述:有 n 个数(其中有些数可能相同),摆成一排。有以下操作:

Q L R 询问:从第 L 到第 R 个数,有几个不同的数。

R P Col 修改:把第 P 个数改成 Col。

输入:第 1 行两个整数 n,m,分别代表初始数量以及操作个数。

第 2 行 n 个整数,分别代表初始数列中第 i 个数。

第 3 行到第 2 + m 行,每行分别一个操作。

输出:对于每一个询问,输出一个数字,代表第 L 到第 R 个数共有几个不同的数。

如果用莫队算法求解,必须离线,先把查询操作和修改操作分别记录下来。记录查询操作的时候,增加一个变量,记录本次查询前做了多少次修改。

如果没有修改,就是基础莫队,一个查询的左右端点是[L, R]。加上修改之后,一个查询表示为(L, R, t),t 表示在查询[L, R]前进行了 t 次修改操作。可以把 t 理解为“时间”,t 的范围是 1 ≤ t ≤ m,m 是操作次数。

从一个查询移动到另一个查询,除了 L、R 发生变化外,还要考虑 t 的变化。如果两个查询的 t 相同,说明它们是基于同样的数列;如果 t 不同,两个查询所对应的数列是不同的,那么就需要补上这变化(直接用暴力法编程)。两个查询的 t 相差越小,它们对应的数列差别越小,计算量也越小,所以对 t 排序能减少计算量。

与基础莫队一样,也可以给出带修改莫队的几何解释。基础莫队的左右端点[L, R],对应平面上的点(x,y) ,带修改的莫队(L, R, t)对应立体空间的(x,y,z)。每个查询对应立体空间的一个点,那么从一个查询到另一个查询,就是从一个点(x1,y1,z1)到另一个点(x2,y2,z2)。计算复杂度仍然是两点之间的曼哈顿距离。

模仿基础莫队的分块思路。定义带修改莫队的排序,按以下步骤执行:

(1)按左端点 L 排序。若左端点 L 在同一个块,执行(2)。L 对应 x 轴。

(2)按右端点 R 排序。若右端点 R 在同一个块,执行 (3)。R 对应 y 轴。

(3)按时间 t 排序。t 对应 z 轴。

左端点 L 所在的块是第 1 查询关键字,右端点 R 所在的块是第 2 关键字,时间 t 是第 3 关键字。

x 方向和 y 方向的分块,把 x-y 平面分成了方格,代表查询的点在方格内、方格间移动。

根据带修改莫队的几何意义,计算算法的复杂度。这里先不采用 的分块方法,而是设一个分块的大小是 B,共有 n/B 个分块。计算 x、y、z 三个方向上的复杂度:

  (1)x 方向的复杂度(左端点指针 L)。在一个区块内,沿着 x 方向一次最多移动 B,所有的区块共有 m 次移动,总计算量 =mB。

  (2)y 方向的复杂度(右端点指针 R)。在一个区块内,沿着 y 方向一次最多移动 B,所有的区块共有 m 次移动,总计算量 =mB。


作为对照,如果分块 B=√n,复杂度是 O(mn),退化成了暴力法的复杂度。

03、树上莫队

基础莫队和带修改的莫队操作的都是一维数组。基于其他的数据结构的问题,如果能转换成一维数组而且是区间问题,那么也能应用莫队算法。

典型的例子是树形结构上的路径问题,可以利用“欧拉序”把整棵树的结点顺序转化为一个一维数组,路径问题也变成了区间问题,就能利用莫队算法求解。下面的简单题体现了这个思路。

Count on a tree II 洛谷 SP10707

题目描述:给定有 n 个结点的数,每个结点有一种颜色。m 次询问,每次询问给出两个结点 u、v,回答从 u 到 v 的路径上有多少个不同颜色的结点。

输入:第一行是 n 和 m,第二行有 n 个整数,第 i 个整数表示第 i 个结点的颜色。下面 n-1 行,每行有两个整数 u、v,表示一个边(u, v)。下面 m 行,每行有两个整数 u、v,表示一个询问,回答从结点 u 到 v 的路径上有多少个不同颜色的结点。

输出:对每个询问,输出一个整数。

数据范围:1 ≤ n ≤ 4×104,1 ≤ m ≤105

数据范围:1≤n≤2×105,1≤m≤105。

思路:

把树的结点用欧拉序转为一维数组

用 DFS 遍历树的结点,有两种遍历方式,得到两种欧拉序:

  (1)在每个结点第一次进和最后一次出都加进序列;

  (2)每遇到一个结点就把它加进序列。

这里用第(1)种形式的欧拉序。下图的例子,欧拉序:{1, 2, 2, 3, 5, 5, 6, 6, 7, 7, 3, 4, 8, 8, 4, 1}。


▍图 8 一棵树

(u, v)上的路径有哪些结点?首先计算出 u、v 的 lca(u, v)(最近公共祖先),然后讨论两种情况:

  (1)lca(u, v) = u 或 lca(u, v) = v,即 u 在 v 的子树中,或者 v 在 u 的子树中。例如 u = 1, v = 6,区间是{1, 2, 2, 3, 5, 5, 6},出现 2 次的结点{2, 5}不属于这条路径,因为它进来了又出去了。只出现一次的结点属于这条路径,即{1, 3, 6}。

  (2)lca(u, v) ≠ u 且 lca(u, v) ≠ v,即 u 和 v 都不在对方的子树上。此时 u、v 之间的路径需要通过它们的 lca,但是 lca 没有出现在 u 和 v 的欧拉序区间内,需要添上。例如 u = 5,v = 8,区间是{5, 6, 6, 7, 7, 3, 4, 8},去掉出现 2 次的结点{6, 7},剩下{5, 3, 4, 8},再加上它们的 lca = 1,得路径{5, 3, 4, 8, 1}。再例如 u = 5,v = 7,区间是{5, 6, 6, 7},去掉 6,剩下{5, 7},再加上它们的 lca = 3,得路径{5, 7, 3}。

本题的求解步骤

  (1)求树的欧拉序,得到一维数组;求任意两个点的 lca。编码时用树链剖分(做两次 DFS)求欧拉序和 lca。

  (2)把题目的查询(u, v)看成一维数组上的查询。题目要求查询(u, v)内不同的颜色,首先查区间(u, v)内只出现 1 次的结点,并加上 u、v 的 lca,得到路径上的所有结点,然后在这些结点中统计只出现 1 次的数字。

  (3)用莫队算法,离线处理所有的查询,然后一起输出。注意分块时,本题的规模是 2n,因为每个结点在欧拉序中出现 2 次;另外每个结点的颜色数值很大,需要离散化。


发布于: 2023-02-22阅读数: 22
用户头像

TiAmo

关注

有能力爱自己,有余力爱别人! 2022-06-16 加入

CSDN全栈领域优质创作者,万粉博主;阿里云专家博主、星级博主、技术博主、阿里云问答官,阿里云MVP;华为云享专家;华为Iot专家;亚马逊人工智能自动驾驶(大众组)吉尼斯世界纪录获得者

评论

发布
暂无评论
秒懂算法 | 莫队算法_算法_TiAmo_InfoQ写作社区