C++ 中带更新和不带更新的 Mo 算法2025 年 5 月 17 日 | 阅读 13 分钟 Mo 算法(Mo's Algorithm)是一种离线算法,它结合了平方根分解,可以有效地回答范围查询、求和、频率计数等操作。它将数组分成大小为 √N(数组大小)的块,从而避免了冗余计算并优化了查询处理。查询首先按块索引排序,然后在每个块内按查询的右端点排序。这种排序方式可以保证在两指针法(左指针和右指针)添加或删除元素到当前范围时,移动次数最少。 在 Mo 算法的经典版本中,数组是固定的,在范围查询之间不进行更新。所有查询都会被排序,然后按高效的顺序处理。使用双指针来维护一个当前范围。通过使用辅助结构(如用于计数不同值的频率数组),元素可以以恒定的时间进行添加和删除。对于具有大量查询的静态数组来说,它非常高效。 当数组不再是静态的(例如,在查询之间发生更新,如点更新)时,我们就需要额外的机制。这些可以通过离线存储并与查询一起处理,或者在线使用辅助数据结构(如线段树或 Fenwick 树)来处理。由于必须插入数组的更新,并且需要有效地回答新数组的范围查询,因此复杂性会进一步增加。 因此,虽然静态算法对于没有修改的固定数组是最优的,但带有更新的算法则牺牲了简单性以换取灵活性。这两种版本都广泛用于有效地解决范围查询问题,而朴素方法速度太慢。 带有更新的 Mo 算法Mo 算法是一种有效的离线算法,用于使用平方根分解在数组上回答范围查询(求和、计数或频率)。在本文考虑的静态情况下,数组是经典形式,但如果存在更新,Mo 算法将被扩展以有效地处理动态变化。 在此版本中,点更新(例如,修改给定的数组位置)与范围查询结合在一起。为了高效地管理更新,该算法采用了两种主要方法:
程序输出 5 6 1 2 1 3 2 Q 1 5 U 3 4 Q 1 5 U 1 2 Q 1 3 Q 2 5 3 4 2 3 说明初始化 该算法和查询从输入数组和查询中读取。初始数组 arr[] 和用于处理的队列(tempArr[])的值将相应填充。currentAnswer 维护范围查询的结果,freq[] 数组包含当前查询范围内元素的频率。 查询和更新
这些操作在两个不同的数组中处理:
排序查询 使用基于 Mo 算法方法的自定义比较函数对查询进行排序。 我们将数组分成大小为 √N 的块。首先,按左端点 L 所在的块对查询进行排序,然后按右端点 R 排序。对于同一块中的两个查询,按其右端点排序以最大程度地减少重新计算。这种排序减少了冗余操作,并最大程度地减少了查询范围指针的移动。 处理更新 使用以下函数应用或回滚更新:
tempArr[] 和 freq[] 将进行调整以反映这些更改,无论使用哪个函数都会修改 tempArr[] 数组。 查询执行 当前范围的两个 L 和 R 指针正在被处理。算法为每个查询(更新指针)匹配范围 [L, R],同时添加/删除必要的元素。results 数组存储每个查询的答案。 复杂度分析时间复杂度 Mo 算法(带离线更新)的时间复杂度主要由三个因素决定:
因此,总时间复杂度为 O((Q + U)√N)。 空间复杂度 存储需求决定了空间复杂度:
因此,总空间复杂度为 O(N + Q + U)。 2. 在线更新:对于更新和范围查询,我们使用线段树或 Fenwick 树(二叉索引树)等数据结构来实时执行它们。 程序输出 3 4 1 3 1 Q 1 5 U 2 4 Q 1 2 U 3 4 9 5 说明
该程序接受查询并为每次查询输出给定范围内不同元素的数量。在更新之后,会处理范围查询并打印结果。 复杂度分析时间复杂度 这里的查询排序时间为 O(Q log Q),其中 Q 是查询数量。每次查询时,我们都会更改范围,由于线段树,这需要 O(log N) 时间。执行所有查询的总成本为 O(Q log N)。更新操作包括更新线段树,也需要 O(log N) 时间进行更新。我们的更新总时间为 O(U log N),其中 U 是更新次数。结果是,总时间复杂度为 O((Q+U)log N)。 空间复杂度 线段树和辅助数据结构决定了空间复杂度。线段树本身使用 O(N) 空间,查询、结果和辅助信息数组也使用 O(N) 空间。此问题的时空复杂度为 O(N + Q + U)。 Mo 算法(不带更新)Mo 算法是一种用于在静态数组上进行多范围查询的快速方案。它最常用的应用是解决诸如查找给定范围的总和、最小值或最大值,或给定范围内不同元素的数量等问题。Mo 算法基于重新排序查询以最小化范围边界(指针 L 和 R)移动的想法。 它通过平方根分解来实现这一点。数组被分成大约 √N 大小的段,查询根据其左端点和右端点进行排序。Mo 算法通过以这种顺序处理查询来帮助减少冗余计算并加快计算速度。 程序输出 5 3 1 2 1 3 2 1 3 2 5 1 5 2 3 3 说明输入 代码的第一部分获取输入值。第一行有两个值:数组大小 n 和查询数 q。接下来的 n 个值构成输入数组 arr[],大小为 n。下一部分读取查询。每个查询包含两个整数 L 和 R,表示需要计算不同元素数量的范围。使用 Query 结构体向量来存储查询。 排序查询 Mo 算法旨在重新排序查询,从而最大限度地减少指针 L 和 R 的移动。 查询按 L/blockSize(左端点所在的块号)以及块内的 R(右端点)进行排序。对于块号为奇数的情况,为了进一步减少移动,查询按 R 降序排序。 添加和删除函数
处理查询
复杂度分析时间复杂度
综合复杂度为 O((Q + N)√N),其中 N 是数组的大小,Q 是查询数量。 空间复杂度 数组、频率数组和查询的存储决定了空间复杂度: 数组需要 O(N) 的空间复杂度。我们还需要 O(N) 的空间用于频率数组(用于计数元素)。存储查询需要 O(Q) 的空间。因此,此算法的空间复杂度为 O(N + Q)。 |
4 Sum(查找最接近总和的四元组)问题属于 k-Sum 问题类别,它们都与查找一组总和等于目标或接近目标的数字相关。在这里,问题是确定四个...
阅读 16 分钟
在本文中,我们将讨论如何在 C++ 中最小化数组之间对应索引处不相等元素的数量。引言 在 C++ 编程中,我们处理一个适用于许多不同场景的主题,从竞争性编程到需要关键数据对齐和减少的现实世界情况...
7 分钟阅读
返回一个表示 n 支队伍最终比赛的字符串。队伍从 1 到 n 排名,排名 1 是最好的队伍,排名 n 是最差的队伍。标签对应于队伍的初始排名。匹配过程代表队伍...
阅读 4 分钟
在 C++ 中,std::call_once 函数确保指定的函数仅执行一次,即使有来自不同线程的多个并发调用。当一个线程使用带有特定标志和函数的 std::call_once 时,它会检查是否有其他线程当前正在执行该...
阅读 4 分钟
+ 在本文中,您将了解 + 及其语法和示例。什么是 std::numpunct_byname? 在 C++ 中,您可以使用 std::numpunct_byname 函数来自定义适合区域设置的数值操作的格式和标点符号。它包含在 C++ 标准库的
阅读 4 分钟
在本文中,我们将讨论 C++ 中的预处理器指令和函数模板。但在讨论它们的区别之前,我们必须了解预处理器指令和函数模板。什么是预处理器指令? 预处理器程序提供预处理器指令,指示编译器处理源...
阅读 4 分钟
简介:BK 树,或 Burkhard-Keller 树,是一种用于高效近似字符串匹配的数据结构。它在拼写检查器、自动完成和 DNA 测序等需要查找与给定查询接近的单词或序列的应用中特别有用。...
14 分钟阅读
为什么我们不能在 C++ 中声明 std::vector<AbstractClass>?概述 C++ 底层标准模板库 (STL) 的几个主要元素之一是动态集合 std::vector,它可以容纳几乎任何类型的结构。它随后提供了一种易于修改且成功的方法...
7 分钟阅读
在 C++ 中,虚函数和内联函数用途不同。虚函数通过允许派生类重写基类函数来支持多态性,从而在运行时产生动态行为。它依赖 vtable 进行函数调用解析,这会引入一些运行时开销。相比之下,内联...
阅读 10 分钟
简单的基于 RAII 的互斥锁 std::lock_guard 在构造时锁定互斥锁,在销毁时释放它,而不提供用户控制。另一方面,std::unique_lock 函数更加灵活,因为它允许所有权转移、定时锁定、手动解锁和延迟锁定。对于...
阅读 10 分钟
我们请求您订阅我们的新闻通讯以获取最新更新。
我们提供所有技术(如 Java 教程、Android、Java 框架)的教程和面试问题
G-13, 2nd Floor, Sec-3, Noida, UP, 201301, India