1. Introduction & Motivation
A Sparse Table answers range-minimum, range-maximum, or range-GCD queries over an array in $O(1)$ time, after a one-time $O(n \log n)$ preprocessing step. That is a remarkable guarantee: no matter how large the query range is, or how many queries arrive, each one costs the same small constant amount of work once the table is built.
The guarantee comes with one hard restriction: the array must be static. A Sparse Table has no supported way to change a single element after it is built without recomputing large portions of the table. If a workload needs both range queries and point or range updates, a Sparse Table is the wrong tool — that job belongs to a Segment Tree, which answers any range query in $O(\log n)$ but also supports updates in $O(\log n)$. This site's Segment Tree coverage lives in Segment Tree section of the Advanced Structures lesson; if updates are part of the workload, start there instead of forcing a Sparse Table to do a job it cannot do.
So the core tradeoff is: Sparse Table gives $O(1)$ query but zero update support (rebuild is $O(n \log n)$); Segment Tree gives $O(\log n)$ query and $O(\log n)$ update. Choose based on whether the array ever changes after the queries begin.
2. Prerequisites
This lesson assumes comfort with arrays and basic asymptotic notation, covered in Part 3: Arrays & Array ADT. It also leans on the fact that every positive integer range length has a largest power of two that fits inside it, and on fast $\lfloor \log_2 n \rfloor$ computation — both are bread-and-butter bit tricks covered in Bit Manipulation: Core Tricks and Interview Patterns. This lesson does not re-derive those bit tricks; it only reuses the result that floor(log2(x)) equals the index of the highest set bit of x, and that it can be built as a lookup table in $O(n)$.
3. Core Insight
Contrast this with sum: $2 + 3 = 5$, but if a range is covered twice, the double-counted overlap breaks the total. That is precisely why the standard Sparse Table trick only works for idempotent operations. Section 7 below covers a variant, the Disjoint Sparse Table, that lifts this restriction by avoiding overlap entirely.
4. A Concrete Example
Range-minimum over an 8-element array
Take arr = [7, 2, 3, 0, 5, 10, 3, 12] (indices 0-7, $n = 8$). Build one row of the table per power of two: sparse[j][i] holds the minimum of the $2^j$ elements starting at index $i$.
| j (block length $2^j$) | sparse[j][i] for valid i |
|---|---|
| 0 (length 1) | [7, 2, 3, 0, 5, 10, 3, 12] |
| 1 (length 2) | [2, 2, 0, 0, 5, 3, 3] — e.g. sparse[1][0] = min(7,2) = 2 |
| 2 (length 4) | [0, 0, 0, 0, 3] — e.g. sparse[2][0] = min(sparse[1][0], sparse[1][2]) = min(2,0) = 0 |
| 3 (length 8) | [0] — sparse[3][0] = min(sparse[2][0], sparse[2][4]) = min(0,3) = 0 |
Now answer query(1, 5), the minimum of [2, 3, 0, 5, 10] (length 5). The largest power of two that fits inside a length-5 range is $2^2 = 4$, so $j = 2$. Cover the range with two overlapping length-4 windows: sparse[2][1] covers indices 1-4, and sparse[2][2] covers indices 2-5. Their union is exactly indices 1-5, and they overlap at indices 2-3 — which is harmless because min is idempotent. Both entries equal 0, so the answer is min(0, 0) = 0, matching the true minimum of [2, 3, 0, 5, 10].
5. Building the Sparse Table
The build is bottom-up: row 0 is the array itself, and each later row combines two overlapping rows from one level down using the recurrence sparse[j][i] = combine(sparse[j-1][i], sparse[j-1][i + 2^(j-1)]). A log_table lookup (built once in $O(n)$) lets the query step avoid computing a logarithm at query time.
6. Range Minimum Query (RMQ) in O(1)
To answer query(left, right) (inclusive), take length = right - left + 1, look up j = log_table[length] (the largest power of two that fits), and combine the two length-$2^j$ windows anchored at left and at right - 2^j + 1. Those two windows always cover the full range, and any overlap between them is harmless because min is idempotent.
GCD and Max Variants
The exact same table structure and the exact same query formula work for range-maximum and range-GCD, because max and GCD are idempotent too: max(x, x) = x and gcd(x, x) = x. Only the combine function changes — swap min for max, or for gcd, everything else stays identical. Only the GCD variant is shown below; the Max variant is the same code with min replaced by max.
7. Disjoint Sparse Table
The remaining puzzle is finding, in $O(1)$, which block level and which midpoint apply to a given (left, right) pair. The answer: the correct block is the smallest one where left and right fall in different halves, which happens exactly at the level of the highest bit where left and right differ — i.e. highest_bit(left XOR right). That value is looked up in a precomputed table, giving $O(1)$ queries. One subtlety is easy to get wrong: left XOR right can exceed n - 1 even though both left and right are valid indices below n, so the lookup table must be sized to 2 * n, not just n, or the lookup goes out of bounds.
8. Complexity Analysis
Disjoint Sparse Table (any associative combine): $O(n \log n)$ preprocessing time and space, $O(1)$ query time, with a larger constant factor and the extra "highest differing bit" table.
Updates: neither variant supports an in-place update. Changing one element can change $O(\log n)$ table entries across different rows, but those entries are not localized the way a Segment Tree's are — the practical answer is a full $O(n \log n)$ rebuild. This is exactly why Sparse Tables are reserved for static arrays: workloads with even occasional writes belong on a Segment Tree instead.
9. Applications & Alternatives
Where Sparse Tables show up
- Lowest Common Ancestor (LCA): flatten a tree with an Euler tour, then answer LCA queries as a range-minimum query (by depth) over the tour in $O(1)$ after $O(n \log n)$ preprocessing — a classic reduction from a tree problem to an array problem.
- Competitive programming range queries: any problem that fixes the array up front and then fires a large batch of range-min, range-max, or range-GCD queries is a strong candidate for a Sparse Table over a Segment Tree, since the query code is simpler and faster in practice.
- String algorithms: range-minimum over an LCP (longest common prefix) array, used inside suffix-array-based string matching, is a textbook Sparse Table application.
10. Exercises and Mastery
Easy
- By hand, build the sparse table for
[5, 1, 4, 2, 8, 3, 9, 6]and answerquery(2, 6)andquery(0, 3)for range minimum. - For an array of length 13, list every power-of-two window length that
log_tablewould ever select for a query, and explain why.
Medium
- Implement range-GCD queries using the sparse table on
[8, 12, 20, 4, 16, 28]and verifyquery(1, 4)by hand against a direct GCD computation. - Explain why a range-sum Sparse Table built the same way as range-minimum would silently give wrong answers, and construct a specific failing example.
Hard
- Sketch how to combine a sparse table with an Euler tour of a tree to answer Lowest Common Ancestor queries in $O(1)$ after $O(n \log n)$ preprocessing. You do not need to write full code — describe: what the Euler tour records at each visit, what array the sparse table is built over (hint: node depths), and how a range-minimum query over the tour maps back to an LCA. A sketch of the update to the RMQ query signature (returning the index of the minimum, not just its value) is welcome but optional.
- Prove that the Disjoint Sparse Table's query never double-counts an element: for any
left < right, show that the block level $k =$highest_bit(left XOR right)placesleftandrightin different halves of the same block, and that the union of the two halves used is exactly $[left, right]$.
Review rhythm: tomorrow, rebuild the sparse table for a small array from memory and answer two queries by hand; next week, implement both the standard and disjoint variants without looking at this page; several weeks later, recognize when a new range-query problem calls for a Sparse Table instead of a Segment Tree or Fenwick Tree.
11. Summary Cheat Sheet
- Sparse Table: $O(n \log n)$ preprocessing, $O(1)$ query, but the array must be static — no supported updates.
- Works only for idempotent combine functions (min, max, GCD), because the $O(1)$ query overlaps two precomputed ranges.
- Build recurrence:
sparse[j][i] = combine(sparse[j-1][i], sparse[j-1][i + 2^(j-1)]). - Query: pick
j = log_table[length], combine the windows atleftand atright - 2^j + 1. - Disjoint Sparse Table: same $O(n \log n)$/$O(1)$ bounds, but works for any associative combine (sum, matrix multiplication) by precomputing non-overlapping prefix/suffix combines per block.
- Disjoint query: find the split level via
highest_bit(left XOR right), sized to2nto avoid an out-of-bounds lookup. - Need updates too? Use a Segment Tree instead — $O(\log n)$ query and update, fully general.