Mos Algorithm In C

What Makes Mo's Algorithm Necessary?

To grasp the relevance of Mo's Algorithm, it is crucial to recognize the constraints of simpler methods. Consider a scenario where there is an array of length n and we need to respond to q range sum inquiries. In such cases, a basic approach involves iterating through the array for each query, resulting in a time complexity of O(n) per query. Consequently, the cumulative time complexity for all q queries would sum up to O(nq). While this approach might be feasible for smaller values of n and q, it becomes inefficient for larger datasets where n and q scale to hundreds of thousands or even millions.

This is the precise moment when Mo's Algorithm becomes crucial. It effectively eliminates unnecessary operations and significantly reduces the time spent on updating the range for every query. The reordering of queries establishes a specific processing sequence that primarily involves making slight adjustments, such as adding or removing elements, instead of recalculating the entire range for each query.

Real-Life Comparative

Imagine you possess an array containing millions of elements, and you aim to compute the sum of various subarrays. Repeatedly calculating the total for each subarray is unnecessary. Instead, you can efficiently traverse the array by "sliding," incorporating new elements only within the specified range while excluding elements falling outside it. This incremental adjustment method forms the essence of Mo's Algorithm.

Without utilizing Mo's Algorithm, you would have to start afresh for each query you want to address in tackling this issue. This could entail recalculating totals that have already been computed for earlier queries. This process is akin to solving a sequence of interconnected puzzles where each puzzle has some components overlapping with the one before, but you must reconstruct each one from the ground up. Conversely, Mo's Algorithm optimizes efficiency by recycling segments of the puzzle, thus saving time and effort.

Applications for Mo's Algorithm

Due to its excellent adaptability, Mo's Algorithm is suitable for a wide range of scenarios that involve performing static range queries on arrays. Its significance in competitive programming lies in its ability to efficiently handle a substantial volume of queries on predetermined arrays. Let's explore a few common use cases where Mo's Algorithm excels:

1. Total Questions

Addressing a vast quantity of range sum inquiries is arguably one of the clearest illustrations of Mo's Algorithm. Assume we have a set of integers and we need to determine the total of the elements within positions L and R for numerous queries. In such a scenario, each query would have had a time complexity of O(n) using a straightforward method, where n represents the array's size. Dealing with extensive datasets might render the total time complexity of O(nq) queries unacceptably sluggish.

Consider a sample array [1, 2, 3, 4, 5] along with two illustrative queries.

How many elements are there from position 0 up to, but not including, position 3?

What is the total of the values from index 1 to index 4?

For the next inquiry, the method of calculating the total is inconsequential due to Mo's Algorithm. When addressing the subsequent question, we deduct the initial sum element from the first query and then include the final element to determine the outcome.

2. Differential Element Inquiries

The primary application appears to involve determining the count of unique elements within a specific subarray. An unrefined approach to tackle this issue could involve establishing a data structure like a set or hash map to record unique elements, followed by scanning the entire subarray for each inquiry. In the broader scope of complexity analysis, this method would operate at O(nq), featuring a time complexity of O(n) for each query.

We can significantly reduce the time complexity using Mo's Algorithm. This algorithm functions by keeping track of the frequency of elements within a specified range. Adjusting the count of elements within the range is done as the query range is modified. When the count changes from 0 to 1, an element is added to the set of unique elements, and when the count goes back to 0, the element is removed from the set. This step-by-step approach allows us to avoid recalculating the number of unique elements for each query. For example, in a range query that identifies the unique elements between index 1 and index 4, the resulting array [1, 2, 1, 3, 4] contains 4 unique elements: 2, 1, 3, 4. To calculate the unique elements between index 0 and index 3 in the next query, we only need to add the first element and update the counts accordingly.

3. Frequency of Query

One can extend the application of Mo's Algorithm to address inquiries regarding the occurrence rate of a specified

  • within a designated range. For instance, to calculate the frequency of a particular number within a defined span, maintaining a frequency array while adjusting the query range can be an effective approach.

These scenarios often involve determining the most common element in a subarray or checking if a specific element exceeds a certain frequency. The efficient solution lies in Mo's Algorithm, which addresses these queries by sorting them and dynamically adjusting frequency counts as the range changes.

4. Subarray Inversions

One simplistic method of calculating the inversions in a subsection of large arrays is inefficient as it examines every possible pair of elements, leading to a time complexity of O(n^2) for each query. However, Mo's Algorithm offers a significant improvement by sorting the queries and systematically adjusting the range while keeping track of the inversion count.

To implement Mo's Algorithm in this scenario, we track the inversion count each time the query range undergoes modification. Essentially, an inversion occurs whenever a new element is added to or removed from the range, requiring comparison with the existing items within the range.

For example, given that 4 is greater than 1, the item in the list at position (4, 1) in [2,4,1,3,5] results in an inversion. By tracking the changes in the inversion count with Mo's Algorithm while adjusting the interval, one can accurately determine the total number of inversions between index 0 and 3.

5. Maximum and Minimum Number of Questions

Answering inquiries regarding the highest or lowest element within a specified range can be effectively addressed by employing a modified version of Mo's Algorithm. While methods such as sparse tables or segment trees are commonly applied in dynamic environments for such purposes, Mo's Algorithm proves to be highly beneficial for swiftly handling static queries, even amidst dynamic conditions.

The concept remains consistent with other scenarios: continuously adjust the maximum or minimum element when altering the query range. This approach eliminates the need to compute the maximum or minimum value for each query from the beginning.

For example, if a query asks for the largest element within the range 1-3, and you have the array [5, 2, 9, 3, 6], Mo's Algorithm simplifies this task by adjusting the interval instead of recalculating the maximum value repeatedly.

When managing large arrays and numerous search operations, Mo's Algorithm provides an effective solution to various static range query issues.

Mo's Algorithm effectively reduces the computational burden on the CPU by handling queries in a step-by-step manner. This efficiency extends beyond basic sum queries to encompass diverse operations such as counting distinct elements, monitoring frequencies, detecting inversions, and determining maximum and minimum values within a subset of data. The static range queries are efficiently managed by competitive programmers using Mo's Algorithm due to its effectiveness, adaptability, and straightforward implementation.

Step 1: Structure to Represent Queries

Example

#include <stdio.h>

#include <stdlib.h>

#include <math.h>

struct Query {

int L, R, idx;

};

// Function to compare queries by block and then by R value

int compare(const void* a, const void* b) {

struct Query* q1 = (struct Query*)a;

struct Query* q2 = (struct Query*)b;

intblockSize = sqrt(n);  // Block size

int block_q1 = q1->L / blockSize;

int block_q2 = q2->L / blockSize;



    // Sort by blocks

if (block_q1 != block_q2) {

return block_q1 - block_q2;

    }

    // Sort by R within the same block

return q1->R - q2->R;

}

Here, a data structure named Query is established to hold individual queries, including the lower and upper range limits denoted as L and R respectively. It also includes an index idx to correlate the outcomes with the queries post sorting.

The sorting algorithm organizes queries initially based on their block and subsequently by their right boundary.

Step 2: Mo's Algorithm Core Logic

Example

voidmoAlgorithm(intarr[], int n, struct Query queries[], int q) {

int *answer = (int *)malloc(q * sizeof(int));  // Store query results

qsort(queries, q, sizeof(struct Query), compare);



intcurrentL = 0, currentR = 0;

intcurrentSum = 0;



    // Process each query

for (inti = 0; i< q; i++) {

int L = queries[i].L;

int R = queries[i].R;



        // Move currentL to L

while (currentL< L) {

currentSum -= arr[currentL];

currentL++;

        }

while (currentL> L) {

currentL--;

currentSum += arr[currentL];

        }



        // Move currentR to R

while (currentR<= R) {

currentSum += arr[currentR];

currentR++;

        }

while (currentR> R + 1) {

currentR--;

currentSum -= arr[currentR];

        }



        // Store the result of the current query

answer[queries[i].idx] = currentSum;

    }



    // Output all answers

for (inti = 0; i< q; i++) {

printf("%d\n", answer[i]);

    }

}

Step 3: Example Usage

Example

int main() {

intarr[] = {1, 1, 2, 1, 3, 4, 5, 2, 8};  // Array of elements

int n = sizeof(arr) / sizeof(arr[0]);



    // Queries for sum of elements in ranges [L, R]

struct Query queries[] = {

        {0, 4, 0}, {1, 3, 1}, {2, 4, 2}

    };

int q = sizeof(queries) / sizeof(queries[0]);



moAlgorithm(arr, n, queries, q);



return 0;

}

Output:

Output

8

4

6

Each query consists of:

  • L: The left endpoint of the query.
  • R: The right endpoint of the query.
  • idx: The index of the query to store the result in the correct order after processing.

moAlgorithm Function:

This function operates on the queries utilizing Mo's Algorithm. It accepts:

Query Processing:

The algorithm maintains two pointers currentL and currentR that define the current range. For each query:

  • Expand or shrink the range by moving currentL and currentR to match the query’s range [L, R].
  • If currentL is smaller than L, elements are removed from the left end, and currentSum is adjusted.
  • If currentL is greater than L, elements are added back from the left side.
  • If currentR is smaller than or equal to R, elements are added from the right side.
  • If currentR is greater than R, elements are removed from the right.

Input Required

This code uses input(). Please provide values below: