1. Introduction & Motivation
Most of the algorithms in this course are online: they must answer each query the instant it arrives, without knowing what will be asked next. A hash map lookup, a segment tree range sum, a binary search — all of these commit to an answer before the next question is even known.
But many real problems hand you the entire batch of queries up front: a competitive programming judge gives you $q$ range queries on an array before you output anything; a batch analytics job receives a full day's worth of range requests before it starts computing; a test harness lists every question it will ask before grading begins. When you are given all the queries in advance, you are not required to answer them in the order they were asked. You are free to reorder them into whatever sequence makes them cheapest to answer collectively — even though each individual query still asks about the same original, unreordered array.
This is the core idea of offline query processing: defer computation, look at the whole workload at once, sort it into a friendlier order, then sweep through it once. It sounds like a small trick, but it is one of the most powerful general techniques in algorithm design, because it turns a large family of "individually expensive, cannot be indexed online" problems into "cheap in aggregate." The flagship application of this idea for range queries on arrays is Mo's Algorithm, which this lesson builds toward after first establishing the general pattern with a simpler offline sweep.
2. Prerequisites
This lesson assumes comfort with arrays, sorting, and basic asymptotic notation. Two companion lessons supply ideas reused directly here:
- Square Root Decomposition introduces the block-size intuition — splitting an array or a query set into blocks of roughly $\sqrt{n}$ size to balance the cost of two different kinds of work. Mo's Algorithm reuses that exact intuition, but applies it to a set of queries rather than to the array's positions.
- Sorting Algorithms covers the comparison-sort machinery ($O(n\log n)$ sorting, custom comparators, stable ordering) that every offline technique in this lesson leans on to reorder its input before sweeping.
3. Sort-by-Threshold and Event Sorting
The simplest version of this pattern does not need blocks or square roots at all: sort by the array index itself. Consider the classic question, "for every position $i$, how many elements before position $i$ are strictly greater than $\text{arr}[i]$?" Answering this naively per index costs $O(n)$ per index and $O(n^2)$ overall. Offline processing fixes this by sweeping the array left to right exactly once while maintaining a running structure of "everything seen so far," answering each index's query the moment the sweep reaches it.
Offline counting: elements greater than me, seen so far
Take arr = [5, 2, 6, 1]. For each index $i$, count how many earlier elements are greater than arr[i]. Sort the four "queries" (one per index) by index ascending — here that is already their natural order — then sweep left to right, inserting each value into a Fenwick tree (Binary Indexed Tree) keyed by value after answering that index's query.
| i | arr[i] | Inserted so far | Count > arr[i] | Insert arr[i] |
|---|---|---|---|---|
| 0 | 5 | {} | 0 | {5} |
| 1 | 2 | {5} | 1 | {5, 2} |
| 2 | 6 | {5, 2} | 0 | {5, 2, 6} |
| 3 | 1 | {5, 2, 6} | 3 | {5, 2, 6, 1} |
The per-index answers are [0, 1, 0, 3]. Their sum, 4, is exactly the array's total inversion count — this offline sweep is a correct, if unglamorous, way to solve inversion counting (the same problem the sorting lesson only names in passing). The trick generalizes to any situation where a batch of "as of position $i$" questions can be answered by a single left-to-right sweep that only ever inserts, never deletes.
A frequency array works for small value ranges; a Fenwick tree (also called a Binary Indexed Tree, or BIT) makes the same sweep work for large or arbitrary value ranges in $O(\log n)$ per insert and per query, after compressing the values to ranks. The implementation below is self-contained — it builds its own small Fenwick tree inline, so you do not need to have studied Fenwick trees separately first. (Advanced Structures: Tries, Union-Find & Segment Trees covers Fenwick trees in more depth, if you want the general-purpose data structure afterward.)
4. Mo's Algorithm: Core Insight
[left, right] pointers one element at a time — adding an element when a pointer grows the window, removing one when a pointer shrinks it. Because the queries are sorted, the total distance every pointer moves across the entire batch of queries is bounded by $O((n + q)\sqrt{n})$, instead of $O(nq)$ for recomputing every query from scratch.
Split the array's indices into blocks of size roughly $\sqrt{n}$. Sort the queries primarily by block(l) = l / block_size, and secondarily by r. Within a block, the left pointer only ever needs to move a total distance of $O(\sqrt{n})$ across all queries in that block (there are $O(\sqrt{n})$ blocks, giving $O(n\sqrt{n})$ left-pointer movement overall), while the right pointer moves monotonically forward within each block, for a total of $O(n)$ movement per block and $O(n\sqrt{n})$ across all blocks. Add movement when switching between blocks and the total across the whole batch is $O((n+q)\sqrt{n})$.
A common refinement is the odd-even trick: sort blocks with an even index by r ascending, and blocks with an odd index by r descending. Without this trick, the right pointer resets to a small value at the start of every new block and re-sweeps forward, wasting movement; alternating the sort direction lets the right pointer glide back down through the next block instead of jumping, which removes that reset cost and speeds up the sweep in practice, without changing the asymptotic bound.
5. A Concrete Example
Range distinct-count over four queries
Array arr = [1, 2, 1, 3, 2] (indices 0-4), block size $= \lfloor\sqrt{5}\rfloor = 2$. Queries ask for the number of distinct values in [l, r] (inclusive, 0-indexed):
- Q1 = [0, 2] → expected answer 2 (values {1, 2})
- Q2 = [1, 4] → expected answer 3 (values {1, 2, 3})
- Q3 = [0, 4] → expected answer 3 (values {1, 2, 3})
- Q4 = [2, 3] → expected answer 2 (values {1, 3})
Block of l: Q1 → block 0, Q2 → block 0, Q3 → block 0, Q4 → block 1. Block 0 is even, so sort by r ascending (ties broken by l): order is Q1 (r=2), Q3 (r=4), Q2 (r=4). Block 1 has only Q4. Processing order: Q1, Q3, Q2, Q4.
| Query | Window before | Pointer moves | Window after | Distinct answer |
|---|---|---|---|---|
| Q1 [0,2] | empty | add arr[0]=1, add arr[1]=2, add arr[2]=1 | [0,2] | 2 |
| Q3 [0,4] | [0,2] | add arr[3]=3, add arr[4]=2 | [0,4] | 3 |
| Q2 [1,4] | [0,4] | remove arr[0]=1 | [1,4] | 3 |
| Q4 [2,3] | [1,4] | remove arr[1]=2, remove arr[4]=2 | [2,3] | 2 |
Every answer matches the brute-force expectation, and the window only ever changed by one element at a time — six additions and three removals total across all four queries, versus recomputing each query's window from an empty state (which would touch 3 + 4 + 5 + 2 = 14 elements). The saving grows with $q$ because sorted queries almost always overlap their neighbors.
6. Standard Mo's Algorithm Implementation
The implementation below reproduces the worked example exactly: it sorts queries by (block of l, then r with the odd-even alternating direction), then answers them with a sliding window whose add/remove callbacks track a frequency map and a running distinct count. The pointer-movement order matters: always grow both pointers outward first (adding elements), and only then shrink both pointers inward (removing elements). This order keeps the window valid at every intermediate step.
7. Mo's Algorithm with Modifications (Mo's with Updates)
The 3D extension
Standard Mo's Algorithm assumes the array never changes. Real problems often interleave range queries with point updates (arr[pos] = newValue). The extension adds a third dimension — time — representing how many updates have happened so far. Every query is recorded as a triple (l, r, t), where t is the count of updates that occurred strictly before that query in the original operation sequence. Queries are then sorted by (block(l), block(r), t), using a block size around $n^{2/3}$ instead of $\sqrt{n}$; this balances the three pointers (left, right, and time) and yields an overall complexity of $O(n^{5/3})$.
Answering a query now requires moving three pointers instead of two: the usual l/r window pointers, plus a t pointer that steps forward through recorded updates (applying them) or backward (undoing them) to bring the array to the state the query expects. An update only changes the answer if its position falls inside the current window: applying update (pos, oldVal, newVal) while pos is inside [curL, curR] means removing oldVal and adding newVal from the running frequency count; if pos is outside the window, the array value still changes, but the frequency count does not.
8. Hilbert-Order Optimization
Sorting by (block, alternating r) is not the only ordering that bounds pointer movement by $O((n+q)\sqrt{n})$. A popular alternative in competitive programming maps each query (l, r) to a point on a Hilbert space-filling curve and sorts queries by their position along that curve instead. The Hilbert curve visits every cell of a grid exactly once while guaranteeing that points close together along the curve are also close together in the 2D grid; sorting by a single "Hilbert index" number therefore keeps consecutive queries close in both l and r simultaneously, without needing the odd-even direction-flipping trick or careful block-boundary tie-breaking.
The fundamental algorithm does not change at all — the same add/remove sliding window from the previous sections still answers the sorted queries. Only the sort key changes, from a (block, r) pair to a single Hilbert-curve index:
def hilbert_index(x, y, order):
"""Map point (x, y) on a 2**order x 2**order grid to its 1D Hilbert-curve index."""
d = 0
s = 1 << (order - 1)
while s > 0:
rx = 1 if (x & s) > 0 else 0
ry = 1 if (y & s) > 0 else 0
d += s * s * ((3 * rx) ^ ry)
if ry == 0:
if rx == 1:
x = s - 1 - x
y = s - 1 - y
x, y = y, x
s >>= 1
return d
# sort queries by their Hilbert index instead of (block, direction):
# queries.sort(key=lambda q: hilbert_index(q.l, q.r, order))
In practice, Hilbert order tends to produce noticeably better cache locality and a lower constant factor than the block-and-direction ordering, because the curve avoids the sharp jumps that can occur at block boundaries. The asymptotic bound is the same class of result, but the tighter practical behavior is why competitive programmers reach for it once Mo's Algorithm becomes a performance bottleneck.
9. Complexity Analysis
Standard Mo's Algorithm: $O((n + q)\sqrt{n})$ total time for $n$ array elements and $q$ queries, using $O(n)$ extra space for the frequency structure.
Naive per-query recomputation: $O(nq)$ — every query rebuilds its answer from an empty window.
An online structure that supports the operation directly: $O(q\log n)$, when such a structure exists (for example, a Fenwick tree answers online range-sum queries in $O(\log n)$ each).
That last comparison is the reason Mo's Algorithm exists at all. Some aggregate operations, such as "count of distinct elements in [l, r]," do not have an efficient online segment-tree or Fenwick-tree solution, because they are not decomposable: knowing the distinct count of the left half and the distinct count of the right half of a range does not tell you the distinct count of the combined range (an element could appear in both halves, or in neither, and you cannot tell which from the two half-answers alone). Range sum, range min, and range max are decomposable — you can merge two sub-answers in $O(1)$ — which is exactly why segment trees and Fenwick trees handle them online. Range-distinct-count, by contrast, has no known $O(\log n)$-per-query online data structure for arbitrary updates and arbitrary ranges. Mo's Algorithm sidesteps the decomposability requirement entirely by never merging sub-answers: it only ever adds or removes one element from a running answer, which works for any operation that supports incremental add/remove, decomposable or not. That is why an offline, sqrt-decomposition-based sweep is sometimes the only practical polynomial-time approach for a given aggregate.
10. Applications & Alternatives
Where offline processing changes the approach
- Range distinct-count / range mode queries: the textbook use case for Mo's Algorithm, since neither operation is easily decomposable.
- Offline inversion counting and order statistics: solved by the sort-and-sweep pattern from Section 3, often combined with a Fenwick tree or merge sort.
- Batched graph or tree queries: techniques such as offline LCA and small-to-large merging on trees apply the same "sort all queries, answer them during one traversal" idea.
- Persistent or versioned queries with updates: Mo's Algorithm with updates (Section 7) handles interleaved point updates when a fully persistent data structure would be overkill.
When an online structure for the exact operation exists — range sum, range minimum, range XOR, and other decomposable aggregates — prefer it: a Fenwick tree or segment tree answers each query as it arrives, supports true online use (queries interleaved with updates in real time), and needs no upfront knowledge of future queries. Reach for Mo's Algorithm specifically when the aggregate cannot be merged in $O(1)$ across sub-ranges, or when a segment-tree-based structure exists in theory but is significantly harder to implement correctly than a sliding add/remove window.
11. Exercises & Mastery
Easy
- Given an array and a batch of offline range-sum queries, explain why plain prefix sums already solve this in $O(1)$ per query after $O(n)$ preprocessing, and why Mo's Algorithm's $O((n+q)\sqrt{n})$ sliding window is unnecessary overhead here. (See Prefix Sums & Difference Arrays for the prefix-sum construction itself — this exercise only asks you to recognize when it suffices, not to re-derive it.)
- Trace the offline-counting sweep from Section 3 on
arr = [3, 1, 2]and confirm the per-index answers by hand.
Medium
- Extend the worked example from Section 5 to
arr = [2, 1, 2, 3, 1, 3]with queries [0,3], [1,5], [2,4], [0,5]. Compute the block assignments, the sorted processing order, and each answer using the standard Mo's Algorithm implementation from Section 6. - Modify the Section 6 implementation to answer "sum of distinct values in the range" instead of "count of distinct values," changing only the
add/removecallbacks.
Hard
- Using the Mo's-with-updates implementation from Section 7, trace
initial_arr = [4, 4, 2]with operations[('Q',0,2), ('U',2,4), ('Q',0,2), ('U',0,1), ('Q',0,1), ('Q',0,2)]by hand, tracking the window, the time pointer, and the frequency map at each step. - Derive why the optimal block size for Mo's Algorithm with updates is $\Theta(n^{2/3})$ rather than $\Theta(\sqrt{n})$, by writing the total cost as a function of block size and minimizing it.
Review rhythm: tomorrow, re-derive the pointer-movement bound $O((n+q)\sqrt{n})$ without looking at this lesson; next week, implement standard Mo's Algorithm from a blank file in all three languages; several weeks later, recognize the same "sort the queries, sweep once" pattern in a new offline problem outside range queries entirely. Finally, teach someone the difference between a decomposable aggregate (fits a segment tree) and one that only supports incremental add/remove (needs Mo's Algorithm).
12. Summary Cheat Sheet
- Offline processing means you have every query in advance and are free to reorder them before answering any of them.
- Sort-and-sweep (Section 3) answers a batch of "as of position $i$" queries in one left-to-right pass using a Fenwick tree, and solves inversion counting as a special case.
- Mo's Algorithm sorts range queries by (block of $l$, then $r$, alternating direction per block) and answers them with a sliding
[l, r]window that only ever adds or removes one element at a time. - Always grow both pointers outward (adding) before shrinking either pointer inward (removing) to keep the window valid mid-transition.
- Standard Mo's Algorithm: $O((n+q)\sqrt{n})$ time. Mo's Algorithm with updates: $O(n^{5/3})$ time using a block size around $n^{2/3}$ and a third "time" pointer.
- Hilbert-curve ordering is a drop-in replacement sort key that improves practical cache locality without changing the fundamental sliding-window algorithm.
- Reach for Mo's Algorithm specifically when the aggregate you need is not decomposable across sub-ranges, so no online segment-tree or Fenwick-tree solution exists.