莫队算法

莫队算法

摘抄于https://blog.csdn.net/weixin_43914593/article/details/108485396

基础莫队算法

莫队算法=离线+暴力+分块
离线与在线的概念。在线是交互式的,一问一答;如果前面的答案用于后面的提问,称为“强制在线”。离线是非交互的,一次性读取所有问题,然后一起回答,"记录所有步,回头再做”。
基础的莫队算法是一种离线算法,它通常用于不修改只查询的一类区间问题,复杂度 O ( n n ) O(n\sqrt{n}) O(nn ) ,没有在线算法线段树或树状数组好,但是编码很简单。下面是一道莫队模板题。

HH项链 洛谷 1972
题目描述:给定一个数量,询问某个区间内不同的数有多少个。
输入:第一行一个正整数 n,表示数列长度。第二行n个正整数 ai。第三行一个整数m,表示HH 询问的个数。接下来 m 行,每行两个整数 L,R,表示询问的区间。
输出:输出m行,每行一个整数,依次表示询问对应的答案。

暴力法

可以用STL的unique函数去重,一次耗时 O ( n ) O(n) O(n),m次查询耗时 O ( m n ) O(mn) O(mn)。或者自己编码,用扫描法遍历区间,计数。
查询一个区间有多少个不同的数
定义一个统计数组cnt[],cnt[x]表示区间内x的个数,定义两个指针表示当前区间,L,R。开始L=1,R=0。
给出一个区间分别将L与R指针一步一步移至区间的左右边界,当右指针指向了一个新值,那么这个值在统计数组对应的位置加一,如果对应位置从0变成了1,那么ans加一。同理左指针指向一个新值那么这个值对应的位置减一,如果对应位置变成了0,那么ans减一。
下面的例子是统计区间[3, 7]内有多少不同的数字,初始指针L=1,R=0。
  
在这里插入图片描述
图(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。
统计多个数组
从上面一次查询可以看出,m次查询只需要重复上述过程m次即可。
我们按字典序将区间按左边界排序,小的在前,对于左边界相同的区间按右边界排序,小的在前。
1)简单情况,区间交错,区间[x1, y1]、[x2, y2]的关系是x1 ≤ x2,y1 ≤ y2。例如下图中,查询两个区间[2, 6]、[4, 8]。
在这里插入图片描述

图(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)。
在这里插入图片描述

区间查询问题的几何解释

洛谷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。
  在这里插入图片描述

莫队算法

莫队算法只通过对排序简单的修改,就把暴力法的时间 O ( n m ) O(nm) O(nm)改进为了 O ( n n ) O(n\sqrt n) O(nn )
(1)暴力法的排序:把查询的区间按左端点排序,如果左端点相同右端点。
(2)莫队算法的排序:把数组分块(分成 n \sqrt n n 块),然后把查询的区间按左端点所在块的序号排序,如果左端点的块相同,再按右端点排序(注意不是右端点所在的块序)。
除了排序不一样,莫队与暴力一样。
那这个排序可以提高效率吗?下面从多种情况下莫队算法的复杂度。
(1)简单情况。区间交错,设区间 [ P 1 , y 1 ] [P_1,y_1] [P1,y1] [ P 2 , y 2 ] [P_2,y_2] [P2,y2]的关系是 P 1 < P 2 , y 1 ≤ y 2 P_1<P_2,y_1\leq y_2 P1<P2,y1y2,其中P1、P2是左端点所在的块。L、R只需要从左到右扫描一次,m次查询的总复杂度是 O ( n ) O(n) O(n)
(2)复杂情况。区间包含,设两个区间查询 [ P 1 , y 1 ] [P_1,y_1] [P1,y1] [ P 2 , y 2 ] [P_2,y_2] [P2,y2]的关系是 P 1 = P 2 P_1=P_2 P1=P2 y 2 ≤ y 1 y_2\leq y_1 y2y1如下图所示。
在这里插入图片描述
此时小区间 [ P 2 , y 2 ] [P_2,y_2] [P2,y2]排在大区间 [ P 1 , y 1 ] [P_1,y_1] [P1,y1]的前面,与暴力法正好相反。在区间内,右指针R从左到右单向移动,不在往复移动。而左指针L发生了回退移动,但是被限制在了一个大小 n \sqrt n n 的块内,每次移动的复杂度是 O ( n ) O(\sqrt n) O(n )的。m次查询总复杂度就是 O ( m n ) O(m\sqrt n) O(mn )
(3)特殊情况:m个查询,端点都在不同的块上,此时莫队算法和暴力法是一样的。但此情况下 m ≤ n m\leq \sqrt n mn ,总复杂度 O ( m n ) = O ( n n ) O(mn)=O(n\sqrt n) O(mn)=O(nn )

莫队算法的几何解释

莫队算法的见图6,这张图透彻说明可莫队算法的原理。图中的每个黑点是一个查询。
图6(1)是暴力法排序后的路径,所有的点按x坐标排序,在复杂情况下,路径沿y方向来回往复,震荡幅度可能非常大(纵向震荡,幅度 O ( n ) O(n) O(n)),导致路径很长。
  图6(2)是莫队算法排序后的路径,它把x轴分成多个区(分块),每个区内的点按y坐标排序,在区内沿x方向来回往复,此时震荡幅度被限制在区内(横向震荡,幅度 O ( n ) O(\sqrt{n}) O(n ),形成了一条比较短的路径,从而实现了较好的复杂度。
  在这里插入图片描述
通过图6(2)可以更清晰地计算莫队算法的复杂度:
  (1)x方向的复杂度。在一个区块内,沿着x方向一次移动最多 n \sqrt{n} n ,所有区块共有m次移动,总复杂度 O ( m n ) O(m\sqrt{n}) O(mn )
  (2)y方向的复杂度。在每个区块内,沿着y方向单向移动,整个区块的y方向长度是n,有 n \sqrt{n} n 个区块,总复杂度 O ( n n ) O(n\sqrt{n}) O(nn )
  两者相加,总复杂度 O ( m n + n n ) O(m\sqrt{n}+n\sqrt{n}) O(mn +nn ) ,一般情况下题目会给出 n = m n = m n=m
  根据图6总结出莫队算法的核心思想:把暴力法的y方向的O(n) 幅度的震荡,改为x方向的受限于区间的 O ( n ) O(\sqrt n) O(n )幅度的震荡,从而减少了路径的长度,提高了效率。
  前面曾提到排序问题,对区间排序是先按左端点所在块排序,再按右端点排序,不是按右端点所在的块排序。原因解释如下:如果右端点也按块排序,几何图就需要画成一个方格图,方格中的点无法排序,实际的结果就是乱序。那么同一个方格内的点,在y方向上就不再是一直往上的复杂度为 O ( n ) O(n) O(n)的单向移动,而是忽上忽下的往复移动,导致路径更长,复杂度变差。见图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;
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
莫队算法是一种基于分块的算法,用于解决一些静态区间查询问题,时间复杂度为 $O(n\sqrt{n})$。以下是一个基于Python的莫队算法的示例代码: ```python import math # 定义块的大小 BLOCK_SIZE = 0 # 初始化块的大小 def init_block_size(n): global BLOCK_SIZE BLOCK_SIZE = int(math.sqrt(n)) # 定义查询操作 def query(left, right): pass # 在这里写查询操作的代码 # 定义添加操作 def add(x): pass # 在这里写添加操作的代码 # 定义删除操作 def remove(x): pass # 在这里写删除操作的代码 # 定义莫队算法 def mo_algorithm(n, q, queries): init_block_size(n) queries.sort(key=lambda x: (x[0] // BLOCK_SIZE, x[1])) left, right = 0, -1 for query in queries: while left > query[0]: left -= 1 add(left) while right < query[1]: right += 1 add(right) while left < query[0]: remove(left) left += 1 while right > query[1]: remove(right) right -= 1 query(query[0], query[1]) ``` 在这段代码中,我们首先定义了一个全局变量 `BLOCK_SIZE`,用于表示块的大小。接着,我们定义了三个操作函数 `query()`、`add()` 和 `remove()`,分别用于查询、添加和删除元素。在 `mo_algorithm()` 函数中,我们首先调用 `init_block_size()` 函数初始化块的大小,然后将查询操作按照块的大小和右端点排序,接着使用双指针维护当前查询区间的左右端点,每次移动指针时调用 `add()` 和 `remove()` 函数更新块的状态,最后调用 `query()` 函数进行查询操作。 请注意,这段代码只是一个示例,具体的 `query()`、`add()` 和 `remove()` 函数的实现取决于具体的问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值