0% completed
Mo's Algorithm is designed to efficiently handle query-based problems, particularly those involving range queries on arrays. In such problems, you are often asked to answer multiple queries about different segments of an array. For example, you might need to find the sum, minimum, maximum, or some other property of elements within a specific range of indices.
Without any optimization, solving each query individually can be quite slow. Imagine having an array of size (N) and (Q) queries. A naive approach, where you process each query independently, would result in a time complexity of (O(N \times Q)). This means that for each query, you might end up scanning large portions of the array, leading to slow performance, especially when both (N) and (Q) are large.
Mo's Algorithm comes into the picture to solve this efficiency. By cleverly reordering the queries and using a technique called Square Root Decomposition, Mo's Algorithm significantly reduces the number of operations required to answer all queries. This brings down the time complexity to approximately (O((N + Q) \times \sqrt{N})), making it much more feasible to handle a large number of queries on a large array.
Mo's Algorithm is particularly useful when dealing with problems where you need to answer many range queries on static arrays. It optimizes the query processing time and is especially beneficial in competitive programming, where efficiency is crucial. Mo's Algorithm is a go-to solution when you need to handle hundreds of thousands of queries on large datasets.
Before we learn Mo's Algorithm, it's important to understand Square Root Decomposition, a key technique that forms the foundation of Mo's Algorithm. Square Root Decomposition is a method used to break down a problem into smaller, more manageable pieces, allowing us to solve certain types of queries more efficiently.
The main idea is to divide an array into blocks of approximately equal size. Each block has a length of (\sqrt{N}), where (N) is the size of the array. By doing this, we can process queries over these blocks more quickly, avoiding the need to repeatedly scan the entire array.
Let’s consider an example to see how Square Root Decomposition works.
Suppose you have an array A = [1, 3, 5, 7, 9, 11, 13, 15] and you want to efficiently answer multiple range sum queries, such as "What is the sum of elements from index 2 to 6?"
Preprocessing:
Query:
sum
to 0.[left, right]
:
i
is at the start of a block and the block lies completely within the query range, add the precomputed sum of the block to sum
and skip to the next block.arr[i]
to sum
and move to the next index.sum
as the result of the query.Return sum:
Preprocessing: The time complexity for the preprocessing step, where we calculate the sum of elements for each block, is (O(N)). This involves iterating through the entire array once.
Query: The time complexity for each query is (\sqrt{N}). This comes from:
We have already covered Square Root Decomposition, which is a fundamental concept for optimizing certain types of range queries. Building on that, let's explore Mo's Algorithm, a more advanced technique that efficiently handles multiple queries on a static array. Mo's Algorithm is particularly useful when you need to process a large number of queries involving subarrays, such as finding the sum, minimum, maximum, or other properties of elements within a given range.
Sorting Queries:
L
). Queries are grouped by the block that their left endpoint falls into. For instance, sort queries with L
values from 0 to \sqrt{N}-1 as one group, then from \sqrt{N} to (2\cdot\sqrt{N}-1) as another group, and so on.R
) in ascending order. This two-level sorting helps minimize the movement of the pointers when transitioning from one query to the next, making the algorithm more efficient.Initialize Variables:
currentLeft
and currentRight
pointers to the beginning of the array (0).currentSum
(or the necessary accumulator) to manage the range starting from the beginning.Process Each Query:
currentLeft
and currentRight
pointers to match the query’s range [L, R]
:
currentRight
is less than R
, move currentRight
to the right and add the element at arr[currentRight]
to currentSum
.currentRight
is greater than R
, move currentRight
to the left and subtract the element at arr[currentRight]
from currentSum
.currentLeft
is less than L
, move currentLeft
to the right and subtract the element at arr[currentLeft]
from currentSum
.currentLeft
is greater than L
, move currentLeft
to the left and add the element at arr[currentLeft]
to currentSum
.currentSum
) for the current query.Output Results:
input = {1, 3, 5, 7, 9, 11, 13, 15}
3
.L
) in blocks, and then by their right index (R
). After sorting:
[2, 6]
belongs 2/sqrt(8) = 0<sup>th</sup> block.[0, 4]
belongs 0/sqrt(8) = 0<sup>th</sup> block.[3, 5]
belongs 3/sqrt(8) = 1<sup>st</sup> block.[2, 6]
, and [0, 4]
belongs to the 0<sup>th</sup> block. So, sort them by r
value. Sorted order of them will be [[0, 4], [2, 6]]
.[[0, 4], [2, 6], [3, 5]]
.Let’s process the queries in the sorted order.
Processing Query 1: Sum from index 0 to 4
currentLeft = 0
, currentRight = 0
, currentSum = 1
(since arr[0] = 1
).currentRight
from 0 to 4, updating currentSum
:
arr[1] = 3
→ currentSum = 4
arr[2] = 5
→ currentSum = 9
arr[3] = 7
→ currentSum = 16
arr[4] = 9
→ currentSum = 25
25
.Processing Query 0: Sum from index 2 to 6
currentLeft
from 0 to 2, updating currentSum
:
arr[0] = 1
→ currentSum = 24
arr[1] = 3
→ currentSum = 21
currentRight
from 4 to 6, updating currentSum
:
arr[5] = 11
→ currentSum = 32
arr[6] = 13
→ currentSum = 45
45
.Processing Query 2: Sum from index 3 to 5
currentLeft
from 2 to 3, updating currentSum
:
arr[2] = 5
→ currentSum = 40
currentRight
from 6 to 5, updating currentSum
:
arr[6] = 13
→ currentSum = 27
27
.45
(Sum from index 2 to 6)25
(Sum from index 0 to 4)27
(Sum from index 3 to 5)The final results in order of the original queries are: 45
, 25
, 27
.
Sorting the Queries:
Q
queries takes O(Q \log Q) time.Processing Each Query:
Mo's algorithm processes each query by adjusting the current range of elements using two pointers (currentLeft
and currentRight
).
On average, each element is added or removed from the current range about \sqrt{N} times, where N
is the length of the array. This results in a time complexity of O((N + Q) \times \sqrt{N}) for processing all queries.
Why (N + Q) \times \sqrt{N}?
N
elements in total, and each can be added or removed O(\sqrt{N}) times, this results in O(N \times \sqrt{N}).Q
queries and each might involve adjusting the range (i.e., moving the pointers), the processing of queries also contributes O(Q \times \sqrt{N}) to the overall complexity.Combining these, the overall time complexity is O(Q \log Q + (N + Q) \times \sqrt{N})) \approx ((N + Q) \times \sqrt{N})).
Overall Space Complexity: O(N + Q)
Mo's Algorithm is applicable in scenarios where:
Examples of problems where Mo's Algorithm is applicable include:
.....
.....
.....