Small Group Tutorials

Here to help students catch up, keep up, and move ahead. Book a consultation here.

How to Learn Mo’s Algorithm: Offline Range Queries, Block Ordering, Window Updates and Locality Optimization

Wait, What? Sometimes the fastest way to answer many range queries is to ignore their original order, rearrange them, and reuse almost all of the work from one query in the next.

Mo’s algorithm is an offline query-ordering technique for static arrays. It maintains one active interval and moves its left and right endpoints through the array. The algorithm is useful when adding or removing one element from the current interval is cheap, but there is no convenient associative summary that lets a Fenwick tree, segment tree or sparse table answer the query directly.

Quick Read

One-sentence answer: Mo’s algorithm answers a batch of offline range queries by sorting them into a locality-friendly order so one mutable window can be adjusted incrementally instead of rebuilding every answer from scratch.

  • Beginner: understand offline queries and one sliding window.
  • Intermediate: implement symmetric add/remove operations and block ordering.
  • Advanced: derive the movement bound, tune block size and use alternating or space-filling-curve orderings.
  • Professional: decide when Mo’s algorithm beats mergeable range structures, handle updates carefully, measure constants and test state transitions rigorously.

1. The Problem Signal: Many Queries, One Static Array

Suppose an array is fixed and you receive many queries [L, R]. Each asks something like “how many distinct values are in this range?” or “what is the sum of squared frequencies inside this range?” A direct solution scans every query interval independently. In the worst case that can cost O(NQ).

A prefix sum solves range sums because subtraction combines perfectly. A segment tree works when two child summaries can be merged into a parent summary. But some frequency-sensitive answers do not have a compact merge operation. Mo’s algorithm targets exactly this awkward middle ground.

2. Offline Means You Are Allowed to Reorder the Work

All queries must be known before processing, and the answer to one query must not depend on answering earlier queries in input order. Store each query’s original index, reorder the queries for computation, then place every result back into its original output position.

This legality check comes before complexity. If the problem is interactive, if queries arrive over time, or if the array changes between queries in the basic formulation, ordinary Mo’s algorithm is not valid.

3. One Active Window

Maintain a current interval [curL, curR] and a data structure representing the answer for exactly that interval. Four pointer moves are possible:

  • extend left: add --curL;
  • extend right: add ++curR;
  • shrink left: remove curL++;
  • shrink right: remove curR--.

The intellectual centre of the method is not the loops. It is designing add(i) and remove(i) so the maintained answer stays exactly correct after every single-element change.

4. Beginner Example: Number of Distinct Values

Keep a frequency array or hash map and an integer distinct. When adding value x, if its frequency changes from 0 to 1, increment distinct. When removing x, if its frequency changes from 1 to 0, decrement distinct. The current answer is always distinct.

This is a perfect first Mo problem because the window state has a visible invariant: freq[x] equals the number of copies of x currently inside the active interval.

5. Why Query Order Matters

If two consecutive queries are [2, 100] and [50000, 90000], almost the entire window must move. If consecutive queries overlap heavily, only a few add/remove operations are needed. Mo’s algorithm therefore turns query ordering into an optimization problem: choose an order that keeps nearby ranges near each other.

6. Classic Square-Root Block Ordering

Choose a block size B. Sort queries first by floor(L / B), then by R. Queries whose left endpoints lie in the same block are processed together. Inside one left block, the right endpoint tends to sweep through the array rather than jumping randomly.

For the common case Q ≈ N, choosing B ≈ √N gives the familiar O((N + Q)√N)-style movement bound when add/remove are O(1). A more explicit cost model is useful: left-pointer movement contributes roughly O(QB), while right-pointer sweeps contribute roughly O(N²/B). Balancing those terms gives B around N/√Q; when Q and N have the same order, that reduces to √N.

7. Alternating the Right-End Direction

A simple practical improvement sorts R ascending in one left block and descending in the next. This “odd-even” or boustrophedon ordering avoids sending the right pointer back to the same end of the array at every block boundary. It usually reduces movement substantially without changing the conceptual method.

8. Hilbert Order: Locality as a Two-Dimensional Problem

Each query can be viewed as a point (L, R) in a two-dimensional plane. A Hilbert space-filling-curve order tries to visit nearby points consecutively, often improving practical locality and reducing pointer travel on some workloads.

Treat Hilbert ordering as an implementation strategy, not magic. Its advantage is workload-dependent, and the simple square-root ordering has a clearer textbook movement proof. Benchmark both when constants matter.

9. The Add/Remove Contract

For every index move, the maintained state must match the new interval exactly. A useful professional rule is to write the invariant before the functions:

STATE represents precisely the multiset of A[curL..curR].

Then test add and remove as inverse transitions wherever the problem semantics allow it. Many Mo bugs are not sorting bugs; they are asymmetric state-update bugs.

10. A Reusable Skeleton

sort queries by mo_order
curL, curR = 0, -1

for (L, R, id) in queries:
    while curL > L: add(--curL)
    while curR < R: add(++curR)
    while curL < L: remove(curL++)
    while curR > R: remove(curR--)
    answer[id] = current_answer()

The four while-loops should be boring. Put problem-specific reasoning into the state representation and the add/remove functions.

11. Choosing a Frequency Representation

  • Small bounded values: direct frequency array is usually fastest.
  • Large integer values: coordinate-compress first, then use an array.
  • Sparse arbitrary keys: hash maps work but increase constants and can make performance less predictable.

Coordinate compression changes labels, not equality. If the query depends only on frequencies or relative identity, compression is often a clean optimization.

12. When Mo’s Algorithm Is a Poor Fit

  • The query has a simple prefix-sum formula.
  • The state merges associatively, making a segment tree or sparse table simpler and faster.
  • Queries must be answered online.
  • Add or remove is expensive enough that √N movement is too costly.
  • The array changes frequently and the basic static formulation no longer matches reality.

Mo’s algorithm is not a universal range-query upgrade. It is a fallback for queries whose answer is easy to maintain locally but hard to merge globally.

13. Compare With the Existing Range-Query Toolbox

  • Prefix sums: O(1) queries after O(N) preprocessing for invertible cumulative summaries.
  • Fenwick tree: excellent for prefix-style sums and point updates.
  • Segment tree: online updates and queries when summaries merge cleanly.
  • Sparse table: static idempotent queries such as min/max with very fast queries.
  • Mo’s algorithm: offline, static, incremental-window state; especially useful for non-mergeable frequency statistics.

14. Mo With Modifications

An advanced variant handles point updates by adding a third coordinate: time. Each query records how many updates have occurred before it. The algorithm moves left, right and time pointers, applying or rolling back updates as needed. Typical block choices move toward N2/3-scale partitions rather than √N.

This version is substantially harder because an update may affect an element currently inside the active range. You must remove its old contribution, change the value, and add its new contribution—or reverse those steps during rollback. Learn static Mo thoroughly first.

15. Tree Mo and Euler Tours

Path queries on trees can sometimes be transformed into interval-like queries using Euler-tour representations, then processed with Mo-style ordering. The mapping is more delicate because vertices may appear twice and the lowest common ancestor may need special handling. This belongs after ordinary Euler tours, LCA and static Mo are already secure.

16. Complexity Is More Than the Big-O Label

The real cost is:

sorting + pointer_moves × cost(add/remove) + answer_extraction.

If each add/remove performs a balanced-tree operation, the √N movement is multiplied by a logarithmic factor. If the state is a flat frequency array, the constants can be very small. Cache locality, branch behaviour and compressed value ranges matter in practice.

17. Correctness Proof Structure

  • Legality: prove queries may be processed in any order.
  • Window invariant: after the pointer loops, the maintained state corresponds exactly to A[L..R].
  • Answer invariant: current_answer() is the correct function of that state.
  • Output restoration: answers are written to their original query IDs.

The sorting order affects efficiency, not the mathematical answer. If correctness changes when you choose a different valid query order, the state transition code is wrong.

18. Common Failure States

  • Forgetting that queries must be offline.
  • Losing the original query index after sorting.
  • Mixing inclusive and half-open interval conventions.
  • Updating the answer after changing a frequency when the formula assumed the old frequency.
  • Writing add and remove as superficially opposite code without proving they restore the same invariant.
  • Using √N automatically when Q differs greatly from N.
  • Assuming Hilbert order is always faster without measuring.
  • Choosing Mo’s algorithm when a simpler mergeable structure already gives O(log N) or O(1) queries.

19. Testing Strategy

For small random arrays, generate random ranges and compare every Mo answer with a brute-force scan. After every pointer movement in debug mode, optionally rebuild the active multiset from A[curL..curR] and compare it with the maintained frequencies. Test empty-start transitions, one-element ranges, duplicate-heavy arrays, all-distinct arrays and repeated identical queries.

Then randomize the processing order deliberately. The answers should remain correct even if performance becomes terrible. This is a powerful way to separate state-correctness bugs from ordering-performance bugs.

20. Practice Ladder: Beginner to Professional

  • Level 1: answer three range queries by brute force and mark their overlap.
  • Level 2: maintain distinct-count state while manually sliding one endpoint.
  • Level 3: implement classic block ordering and restore original answer order.
  • Level 4: derive O(QB + N²/B) pointer movement and choose B for your N and Q.
  • Level 5: add odd-even right ordering and measure pointer movement.
  • Level 6: compare block order with Hilbert order on several query distributions.
  • Level 7: implement Mo with modifications, including apply/rollback of point updates.
  • Level 8: benchmark Mo against a segment tree or other specialized method and justify the production choice.

21. How to Learn This Efficiently

Begin with tracing, not code generation. Put four queries on paper, predict which query order will cause less movement, then run a small correct skeleton and inspect the window after each move. Next modify only the add/remove subgoal for a new statistic. Finally build a full solution from scratch. This read–predict–run–investigate–modify–make progression aligns well with PRIMM-style programming pedagogy.

Parsons-style exercises also work well here: scramble the four pointer loops, or provide a faded add/remove function and ask the learner to restore the invariant. Subgoal labels such as “expand window,” “contract window,” “update statistic” and “restore output order” keep syntax from hiding the algorithmic jobs.

22. Learning Hall Boundary

This article owns the public educational job of teaching Mo’s algorithm and offline range-query locality. It complements, rather than replaces, the existing range-query, streaming, online-algorithm and data-structure articles. It does not redefine MindOS, Bolt or Student/Studying Interface jobs and does not expose private eduKateAI architecture, prompts, routing, benchmarks, scoring or implementation details.

Sources and Further Reading

  • Sqrt Decomposition, Algorithms for Competitive Programming (cp-algorithms), current reference covering classic Mo ordering and implementation.
  • Modern range-query references that derive block-size choices from total pointer movement and discuss alternating endpoint orderings.
  • Sue Sentance, Jane Waite and Maria Kallia, PRIMM programming-education research, SIGCSE and Computer Science Education, 2019.
  • Barbara Ericson and colleagues, research and reviews on Parsons problems in introductory computer-science education.
  • Lauren E. Margulieux, Briana B. Morrison and Adrienne Decker, work on subgoal-labelled worked examples in introductory programming, 2020.

Professional rule: choose Mo’s algorithm when reordering is legal and local add/remove updates are dramatically easier than merging range summaries; otherwise prefer the simpler online structure.