Sparse Tables and Range Minimum Query
Range Minimum Query (RMQ) asks: given an array that never changes, answer
thousands of “what’s the smallest value between index l and r?”
questions as fast as possible. A segment tree answers each query in
— fast, but not the fastest possible. When the array is
static (no updates, ever) and the operation is idempotent (like
min, max, or gcd), a sparse table answers every query in after
one preprocessing pass. This is one of the first “free
speedups” competitive programmers reach for the moment updates aren’t in
the problem.
What you’ll learn
Section titled “What you’ll learn”- The RMQ problem and why a static array unlocks a faster structure than a segment tree.
- Building a sparse table: precomputing the answer for every range whose length is a power of two.
- Answering a query in by covering
[l, r]with two overlapping power-of-two ranges — and why the overlap is completely harmless for idempotent operations. - Why sparse tables can’t support updates, unlike a segment tree or Fenwick tree.
- The brief connection between RMQ and LCA (lowest common ancestor) via an Euler tour.
The cue
Section titled “The cue”Building the table: powers of two
Section titled “Building the table: powers of two”For every starting index i and every power j, precompute the answer for
the range that starts at i and spans elements:
Row 0 is just the array itself (ranges of length ). Row j is
built from row j - 1 by combining two halves that are each long
— so the whole table is rows deep, each row an array scan:
total.
def build_log_table(n):
log = [0] * (n + 1)
for i in range(2, n + 1):
log[i] = log[i // 2] + 1 # log[i] = floor(log2(i)), built incrementally
return log
def build_sparse_table(arr):
n = len(arr)
log = build_log_table(n)
k = log[n] + 1 # number of rows needed
st = [arr[:]] # st[0] = ranges of length 2^0 = 1
for j in range(1, k):
half = 1 << (j - 1) # length of each half: 2^(j-1)
row = []
for i in range(n - (1 << j) + 1):
row.append(min(st[j - 1][i], st[j - 1][i + half]))
st.append(row)
return st, log
def query_min(st, log, l, r):
"""Inclusive range [l, r], 0-indexed."""
j = log[r - l + 1]
return min(st[j][l], st[j][r - (1 << j) + 1])
arr = [7, 2, 3, 0, 5, 10, 3, 12, 18]
st, log = build_sparse_table(arr)
print("min of [1, 5]:", query_min(st, log, 1, 5)) # expect 0
print("min of [3, 3]:", query_min(st, log, 3, 3)) # expect 0
print("min of [6, 8]:", query_min(st, log, 6, 8)) # expect 3Querying in O(1): two overlapping windows
Section titled “Querying in O(1): two overlapping windows”A query [l, r] of length len is covered by two windows of length
where : one starting at l,
one ending at r. These two windows almost always overlap in the middle —
and that overlap is exactly why this only works for idempotent
operations. min(min(x, x), y) == min(x, y), so double-counting the
overlapping middle changes nothing. The same trick would silently break for
sum, which double-counts real values.
Note that j is looked up from a precomputed log table in O(1) —
recomputing math.log2 on every query would still be O(1) in theory but
slower in practice and prone to floating-point edge cases at exact powers
of two.
The same idea works for any idempotent, associative operation —
swap min for max or math.gcd and everything else stays identical:
import math
def build_sparse_table_gcd(arr):
n = len(arr)
log = [0] * (n + 1)
for i in range(2, n + 1):
log[i] = log[i // 2] + 1
k = log[n] + 1
st = [arr[:]]
for j in range(1, k):
half = 1 << (j - 1)
row = []
for i in range(n - (1 << j) + 1):
row.append(math.gcd(st[j - 1][i], st[j - 1][i + half]))
st.append(row)
return st, log
def query_gcd(st, log, l, r):
j = log[r - l + 1]
return math.gcd(st[j][l], st[j][r - (1 << j) + 1])
arr = [12, 18, 30, 42, 8, 16]
st, log = build_sparse_table_gcd(arr)
print("gcd of [0, 2]:", query_gcd(st, log, 0, 2)) # gcd(12, 18, 30) = 6
print("gcd of [3, 5]:", query_gcd(st, log, 3, 5)) # gcd(42, 8, 16) = 2Dry run
Section titled “Dry run”arr = [7, 2, 3, 0, 5, 10, 3, 12, 18], range minimum. Row j holds the minimum of every window
of length :
| row | window length | contents |
|---|---|---|
j=0 | 1 | 7 2 3 0 5 10 3 12 18 — the array itself |
j=1 | 2 | 2 2 0 0 5 3 3 12 |
j=2 | 4 | 0 0 0 0 3 3 |
j=3 | 8 | 0 0 |
Each row is built from the one above by combining two halves: st[2][4] = min(st[1][4], st[1][6]) = min(5, 3) = 3. Rows get shorter because a window of length starting at i must fit —
entries.
Query [2, 7] inclusive — six elements: 3 0 5 10 3 12, minimum 0.
| step | value |
|---|---|
| length | 7 − 2 + 1 = 6 |
j = floor(log2(6)) | 2 — the largest power of two that fits |
| left window | st[2][2] = min of arr[2..5] = 0 |
| right window | st[2][7 − 4 + 1] = st[2][4] = min of arr[4..7] = 3 |
| answer | min(0, 3) = 0 ✓ |
- The two windows overlap —
arr[4..5]is in both — and that is fine only because min is idempotent. Counting an element twice does not change a minimum. Try the same structure for a range sum and the overlap is added twice, which is why sums use prefix arrays instead. This is the single most important sentence about sparse tables. - Two lookups, no loop. The query does not depend on the range length at all: any range, however long, is two array reads and one comparison. That is the .
jis the largest power of two that fits, not the smallest that covers. Length 6 uses windows of 4, which together span 8 positions — deliberately more than the range, anchored at both ends so the union is exactly[2, 7].- Precompute the logs.
log[i] = log[i // 2] + 1builds the whole table in one pass; callingmath.log2per query is slower and invites floating-point edge cases at exact powers of two.
Complexity
Section titled “Complexity”| Structure | Build | Query | Update | Best for |
|---|---|---|---|---|
| Sparse table | Not supported | Static array, idempotent op (min/max/gcd) | ||
| Segment tree | Any associative op, array does change | |||
| Fenwick tree (BIT) | Prefix sums and sum-like aggregates |
The variant map
Section titled “The variant map”| Need | Structure | Query | Note |
|---|---|---|---|
| Static range min / max | sparse table | the base case; overlap is free | |
| Static range gcd | sparse table | gcd is idempotent too — gcd(x, x) = x | |
| Static range sum | prefix-sum array | not a sparse table: sums double-count the overlap | |
| Static range sum, if you insist | sparse table with disjoint windows | possible, but the prefix array is strictly better | |
| Mutable range min | segment tree | a sparse table cannot be updated without a rebuild | |
| Mutable range sum | Fenwick tree | invertible, so prefix subtraction works | |
| LCA on a static tree | Euler tour → RMQ over depths | per query | the classic reduction; compare binary lifting at |
| 2-D static range min | sparse table over both dimensions | memory — usually too much | |
| Range min with few queries | just loop the range | below ~ queries the build never pays for itself | |
| Sliding window min (fixed width) | monotonic deque | total | beats both when the window only moves forward |
Interview follow-ups
Section titled “Interview follow-ups”| They ask | What they’re checking | The answer |
|---|---|---|
| “Why can the two query windows overlap?” | The precondition | Because min is idempotent — counting an element twice does not change the answer. That is exactly why the same structure cannot answer range sums |
| “Sparse table or segment tree?” | Judgement | Sparse table if the array is static: per query beats . Segment tree the moment anything is updated — a sparse table has no update, only a full rebuild |
| “What is the preprocessing cost, and when is it worth it?” | Cost modelling | time and memory. It pays off once the query count exceeds roughly ; for a handful of queries, scanning the range directly is cheaper and simpler |
“How do you compute j fast?” | Practical detail | Precompute log[i] = log[i // 2] + 1 in one pass. math.log2 per query is slower and can misbehave at exact powers of two |
| “Range sum with a sparse table?” | The trap | Not with overlapping windows — the intersection is counted twice. Use a prefix-sum array (, less memory), or disjoint sparse-table windows at |
| “Reduce LCA to RMQ” | Composition | Euler-tour the tree recording depth at each visit; the LCA of u and v is the shallowest node in the depth array between their first occurrences — a range minimum, so per query after the build |
| “Memory is tight at ” | Scale sense | The table is entries — about 20 million at , likely too much. Use a segment tree (), or block decomposition / sqrt decomposition if queries are acceptable |
| “The window slides forward one step at a time” | Choosing the cheaper tool | A monotonic deque gives all window minima in total with space — no preprocessing at all. Sparse tables are for arbitrary ranges, not sliding ones |
Practice — real LeetCode problems
Section titled “Practice — real LeetCode problems”Sparse tables answer idempotent range queries — min, max, gcd, AND, OR — in after an build. These three are all “range something over a window”, and each has a cheaper structure hiding behind it. Learning to spot the cheaper one is as valuable as knowing the table.
LC 2419 — Longest Subarray With Maximum Bitwise AND · Medium
Section titled “LC 2419 — Longest Subarray With Maximum Bitwise AND · Medium”Problem. Among all subarrays, consider those whose bitwise AND is the largest achievable. Return the length of the longest such subarray.
Constraints. 1 <= len(nums) <= 10**5, 1 <= nums[i] <= 10**6.
Examples. [1,2,3,3,2,2] gives 2 — the maximum AND is 3, from [3,3] ·
[1,2,3,4] gives 1
Editorial · approach, complexity, follow-ups
Included as a deliberate anti-lesson: this problem looks like range-AND queries and is actually two lines.
The argument. For any subarray, a & b <= min(a, b), because AND only ever
clears bits. So every subarray’s AND is at most max(nums). Can that be achieved?
Yes — a length-1 subarray holding the maximum. And it is achieved only by
subarrays whose every element equals the maximum, since including anything smaller
would drop the value below it. Therefore the answer is the longest run of the
maximum.
Time , two passes. Space .
- Runs, not counts.
[1,2,3,3,2,2]has two 3s and they happen to be adjacent; if the input were[3,1,3]the answer would be 1, not 2. Counting occurrences instead of measuring runs is the wrong answer. - The maximum can be unique, giving 1 — as in
[1,2,3,4]. - All equal gives the whole length.
nums[i] >= 1by constraint, so there is no zero-versus-empty subtlety.
When a sparse table would actually be needed. If the question asked for the AND of arbitrary given ranges, or for the longest subarray whose AND equals some target, the bound no longer collapses the problem. Then either build a sparse table — AND is idempotent and associative, so overlapping windows are safe — or use the set-of-window-values trick from the next problem.
Follow-ups you should expect: “Longest subarray with maximum bitwise OR?” —
different: OR only ever sets bits, so the maximum is the OR of the whole array
and the answer relates to the shortest prefix achieving it. “Maximum AND of exactly
k elements?” — a greedy from the high bit down, keeping candidates. “Arbitrary
range-AND queries?” — sparse table, per query. “The subarray with AND
closest to a target?” — the next problem.
LC 1521 — Find a Value of a Mysterious Function Closest to Target · Hard
Section titled “LC 1521 — Find a Value of a Mysterious Function Closest to Target · Hard”Problem. The function computes the bitwise AND of a subarray arr[l..r].
Return the minimum possible value of abs(func(arr, l, r) - target) over all
subarrays.
Constraints. 1 <= len(arr) <= 10**5, 1 <= arr[i] <= 10**6,
0 <= target <= 10**7.
Examples. arr = [9,12,3,7,15], target = 5 gives 2 ·
arr = [1000000,1000000,1000000], target = 1 gives 999999 ·
arr = [1,2,4,8,16], target = 0 gives 0
Editorial · approach, complexity, follow-ups
The technique — maintain the set of results of all subarrays ending at i —
works for any operator that is monotone in one direction. AND qualifies: extending
a subarray leftward can only clear bits, so along the chain of subarrays ending at
i the value only ever loses bits.
Why the set is small. Order those subarrays by increasing length; each AND is a
submask of the previous, so each distinct value drops at least one bit. With values
under that is at most 20 bits, so at most about 20 distinct values at any
index — independent of n.
Time , roughly operations. Space .
{n} | ...is what starts the length-1 subarray ati. Omitting it misses every single-element answer, including the whole third example.- Check the value as you generate it. The best answer may come from any length, not just the longest.
- Monotonicity does not give you binary search. The values decrease along the
chain, but
abs(v - target)is not monotone, so you must examine all of them — a subtle point and a common wrong optimisation. - A single element returns
abs(arr[0] - target), and the initialisation handles it before the loop. [1000000]*3with target 1 gives 999999: every AND equals 1000000, and nothing gets closer.
The sparse-table alternative. Build a sparse table for range AND, then for each
left endpoint binary search the right endpoint — valid because the AND is monotone
non-increasing in r. That is and is the answer if you are asked
to “use a sparse table”. The set method is faster and shorter, but knowing both,
and knowing that the monotonicity is what licenses the binary search, is the
complete answer.
Follow-ups you should expect: “OR instead of AND?” — the same technique; OR only sets bits, so the set is still . “GCD of a subarray closest to a target?” — also works, since each step at least halves the gcd. “Sum instead?” — breaks completely: sums do not lose bits, so the set is . Use prefix sums plus a sorted structure. “Count the subarrays whose AND equals a target?” — carry counts alongside the set values.
LC 1793 — Maximum Score of a Good Subarray · Hard
Section titled “LC 1793 — Maximum Score of a Good Subarray · Hard”Problem. The score of a subarray nums[i..j] is
min(nums[i..j]) * (j - i + 1). A subarray is good if i <= k <= j. Return
the maximum score of a good subarray.
Constraints. 1 <= len(nums) <= 10**5, 1 <= nums[i] <= 2 * 10**4,
0 <= k < len(nums).
Examples. nums = [1,4,3,7,4,5], k = 3 gives 15 — the subarray [4,3,7,4,5]
has minimum 3 and length 5 · nums = [5,5,4,5,4,1,1,1], k = 0 gives 20
Editorial · approach, complexity, follow-ups
Every good subarray contains k, so the family of candidates is nested: exactly
one window of each width from 1 to n is worth considering, and the greedy visits
precisely those.
Why expanding toward the taller neighbour is safe. Suppose the optimal window
has width w. The greedy also produces some window of width w, and at every
step it chose the neighbour that keeps the running minimum as high as possible. By
induction, the greedy’s window of width w has a minimum at least as large as any
other width-w window containing k — and since the score is
minimum × width, at equal width the higher minimum wins. So the greedy’s
width-w window scores at least as well as the optimum, and it was considered.
That exchange argument is what an interviewer is listening for; the code is easy.
Time . Space .
- Update
floorwith both ends,min(floor, nums[left], nums[right]). Only one moved, but writing both is harmless and removes a branch you can get wrong. >=versus>in the tie-break does not matter — with equal neighbours either choice leaves the minimum unchanged.- The boundary guards come first. Once one side is exhausted the other must
move, and testing
left == 0before comparing avoids indexing at-1, which in Python would silently read the last element rather than crash. That is a genuinely dangerous bug here. - A single element returns
nums[0]and the loop never runs. k = 0works with no special case; the left guard fires immediately.
Two other solutions worth naming. A monotonic stack finds, for each element
as the minimum, the widest window in which it is the minimum — keep those windows
that contain k. Or binary search plus a sparse table: for each candidate
minimum, find the widest range where all values are at least it. Both are
or and are the standard alternatives; the two-pointer greedy is
the shortest.
Follow-ups you should expect: “Without the constraint that the window contains
k?” — that is Largest Rectangle in Histogram, so monotonic stack. “Maximise
min × length for exactly length w?” — sliding-window minimum with a deque.
“Sum instead of minimum?” — prefix sums, a completely different problem. “Prove
the greedy?” — the exchange argument above; be ready, because “why is this not
just a guess” is the follow-up on every greedy.
LeetCode problem set
Section titled “LeetCode problem set”Generated from the problem database, so each entry carries its sheet membership and reported companies. Tick them off as you go — progress is saved in this browser, and the Export button writes it to a file you can keep.
Work down the ladder. Tick each problem off as you go — progress is saved in this browser, and the Export button in the filter bar writes it to a file you can keep.
- 303Range Sum Query - Immutableeasy(Easy) -- the non-idempotent analog: since `sum` isn't idempotent, a plain prefix-sum array (not a sparse table) is the right $O(1)$-query structure for a static array of sums
- 239Sliding Window Maximumhard(Hard) -- a *different* tool for a *different* shape of problem: the window slides by one each step instead of jumping to arbitrary `[l, r]` pairs, so a monotonic deque wins here over a sparse table
Self-check
Section titled “Self-check”-
Why is it safe for the two query windows to overlap?
This single property is what buys the O(1) query, and it is exactly why the same structure cannot answer range sums: the overlap would be added twice.
pch.quizShowAnswer
B — Because min is idempotent — min(x, x) = x — so counting the overlapping elements twice cannot change the answer — This single property is what buys the O(1) query, and it is exactly why the same structure cannot answer range sums: the overlap would be added twice.
-
Can a sparse table answer range SUM queries in O(1)?
Disjoint-window sparse tables can do sums at O(log n), but there is no reason to: prefix sums already answer static range sums in O(1) with O(n) memory.
pch.quizShowAnswer
B — No — sums are not idempotent, so the overlapping windows double-count. Use a prefix-sum array, which is O(1) anyway and uses less memory — Disjoint-window sparse tables can do sums at O(log n), but there is no reason to: prefix sums already answer static range sums in O(1) with O(n) memory.
-
For the range [2, 7] (length 6), which j does the query use and why?
The two windows span 8 positions between them, deliberately more than the range; anchoring at both ends makes their union exactly the range.
pch.quizShowAnswer
B — j = 2, the LARGEST power of two that FITS — two windows of length 4 anchored at each end cover [2,7] exactly, overlapping in the middle — The two windows span 8 positions between them, deliberately more than the range; anchoring at both ends makes their union exactly the range.
-
The array will be updated between queries. Sparse table?
Static-versus-mutable is the whole decision in this phase: sparse table (static, O(1)), segment tree (mutable, O(log n)), Fenwick (mutable + invertible, less code).
pch.quizShowAnswer
B — No — there is no update operation; a change invalidates O(n log n) precomputed windows, so you would rebuild from scratch. Use a segment tree — Static-versus-mutable is the whole decision in this phase: sparse table (static, O(1)), segment tree (mutable, O(log n)), Fenwick (mutable + invertible, less code).
-
How should `j = floor(log2(length))` be computed?
`(length).bit_length() - 1` also works and is exact. The point is to avoid float log in a hot query path.
pch.quizShowAnswer
B — Precompute a log table with log[i] = log[i // 2] + 1 in one O(n) pass — it is faster and avoids floating-point edge cases at exact powers of two — `(length).bit_length() - 1` also works and is exact. The point is to avoid float log in a hot query path.
-
You need the minimum of every sliding window of fixed width k. Sparse table?
O(n log n) preprocessing plus n queries versus a single O(n) pass. Matching the structure to how the ranges are shaped — arbitrary versus sliding — is the judgement being tested.
pch.quizShowAnswer
B — It works, but a monotonic deque gives all of them in O(n) total with O(k) space and no preprocessing — sparse tables are for arbitrary ranges, not sliding ones — O(n log n) preprocessing plus n queries versus a single O(n) pass. Matching the structure to how the ranges are shaped — arbitrary versus sliding — is the judgement being tested.
Recall card
Section titled “Recall card”- Cue — static array, many range min / max / gcd queries. No updates anywhere in the problem.
- Build —
st[j][i]= answer for the window of length starting ati;st[j][i] = op(st[j-1][i], st[j-1][i + 2^(j-1)]). time and memory. - Query
[l, r]—j = floor(log2(r - l + 1)), thenop(st[j][l], st[j][r - 2^j + 1]). Two lookups, no loop, . - The windows overlap and that is fine — only because the operation is idempotent. This is the whole precondition.
- Not for sums — the overlap double-counts; prefix sums are anyway.
- No updates — any change means a full rebuild. Mutable → segment tree.
- Precompute logs with
log[i] = log[i//2] + 1, or usebit_length() - 1. - Unlocks LCA via Euler tour + RMQ over the depth array.
- A sparse table precomputes the answer for every range whose length is a power of two: rows and space, built once.
- Any query
[l, r]is covered by two overlapping power-of-two windows, answered in — the overlap only stays correct becausemin/max/gcdare idempotent. - It cannot support updates — reach for a segment tree or Fenwick tree the moment the array changes.
- The same RMQ machinery, run over an Euler tour’s depth array, answers LCA queries in too.
Next: Number Theory for Competitive Programming — modular arithmetic, fast exponentiation, sieves, and the combinatorics-mod-p toolkit that shows up in nearly every CP contest.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading