queries on a static array after preprocessing. The plain table needs an idempotent operation (: min, max, gcd, AND, OR), because the two query blocks overlap. The disjoint variant handles any associative operation in .
Sparse table
aggregates , and a query takes two overlapping blocks of length .
template<class T, class F> struct SparseTable {
vector<vector<T>> sp; F f;
SparseTable(const vector<T>& a, F f) : f(f) {
int n = sz(a), K = __lg(max(n, 1)) + 1;
sp.assign(K, a);
for (int k = 1; k < K; k++)
for (int i = 0; i + (1 << k) <= n; i++) sp[k][i] = f(sp[k - 1][i], sp[k - 1][i + (1 << (k - 1))]);
}
T query(int l, int r) const { // [l, r), l < r
int k = __lg(r - l);
return f(sp[k][l], sp[k][r - (1 << k)]);
}
};
// SparseTable st(a, [](int x, int y) { return min(x, y); });Layout sp[k][i] (level-major) keeps each level contiguous, so the build is cache-friendly. For with 20 levels of int, that’s 80 MB; drop unneeded levels or switch to the RMQ below.
Uses: LCA via Euler tour + RMQ; LCP of two suffixes via RMQ on the LCP array; range gcd; static range min inside binary search or two-pointer loops; “first position to the right with value ” by binary lifting on the table ( descent over levels).
Proof: the overlapping-block query is correct for idempotent
by induction: the build line combines two length- blocks covering exactly that range. For a query with , both blocks and lie inside (since ) and together cover all of (since forces , so the two blocks overlap rather than leaving a gap). Every element of appears in at least one block, and elements in the overlap appear in both. Idempotence () means repeating an element in the fold doesn’t change the result — formally, idempotent and commutative/associative on a multiset extends to “folding any covering multiset containing every element at least once gives the same answer as folding the set”, since duplicate elements collapse. So , which folds the two (possibly overlapping) blocks, equals the fold over exactly.
Disjoint sparse table
At level , cut the array into blocks of length ; inside each block store suffix aggregates leftwards from the middle and prefix aggregates rightwards from it. For (inclusive), the level is the highest bit where and differ: they’re in the same block on opposite sides of its middle.
template<class T, class F> struct DisjointSparseTable {
vector<vector<T>> t; vector<T> a; F f;
DisjointSparseTable(const vector<T>& a_, F f) : a(a_), f(f) {
int n = sz(a), K = max(1, __lg(max(n - 1, 1)) + 1);
t.assign(K, vector<T>(n));
for (int k = 0; k < K; k++) {
int half = 1 << k;
for (int mid = half; mid < n + half; mid += 2 * half) {
if (mid - 1 < n) {
t[k][mid - 1] = a[mid - 1];
for (int i = mid - 2; i >= mid - half; i--) t[k][i] = f(a[i], t[k][i + 1]);
}
if (mid < n) {
t[k][mid] = a[mid];
for (int i = mid + 1; i < min(n, mid + half); i++) t[k][i] = f(t[k][i - 1], a[i]);
}
}
}
}
T query(int l, int r) const { // inclusive [l, r]
if (l == r) return a[l];
int k = __lg(l ^ r);
return f(t[k][l], t[k][r]);
}
};Proof: disjoint sparse table query correctness
Let , the index of the highest bit where and differ ( so ). Bits above agree between and ; write that shared high part as (a multiple of after shifting). Since bit differs and is the highest differing bit, has bit equal to and has it equal to (as ), so and — i.e. both lie in the same length- block used at level , with strictly left of that block’s midpoint and at or right of it. By construction, is the suffix-aggregate from up to the midpoint, and is the prefix-aggregate from the midpoint up to , so folds exactly with no gap and no overlap (the two aggregates meet exactly at the midpoint, each element counted once).
Tested against a brute-force fold over random associative non-idempotent operations (e.g. sum, matrix product) and random , confirming it handles non-idempotent ops where the plain overlapping sparse table would double-count.
Uses: range products modulo a composite, range matrix products, range “max subarray sum” nodes, and huge numbers of queries () where a segment tree’s would dominate. The same “prefix and suffix from the middle” idea is the offline divide and conquer for range DP queries (Divide and Conquer).
2D sparse table
covers the rectangle . A query uses four overlapping rectangles, so it needs idempotence. Build and memory are : a grid of int already needs about 80 MB. Alternatives: a 2D sliding window max with deques for fixed-size windows, per-row sparse tables with queries, or a 2D segment tree.
Choosing a static structure
| Operation | Structure | Query |
|---|---|---|
| sum, XOR (invertible) | prefix sums | |
| min, max, gcd, AND, OR | sparse table, or RMQ (Farach-Colton-Bender, see RMQ) | |
| any associative | disjoint sparse table, sqrt tree | |
| associative, low memory | segment tree | |
| -th smallest, counts by value | wavelet matrix, persistent segment tree |
Choosing a range-query structure in general
The full (update, query) matrix — the decision this whole chapter answers:
| Updates | Queries | Structure | Cost |
|---|---|---|---|
| none | range query, idempotent op | sparse table / disjoint sparse table / sqrt tree | |
| none | range query, non-idempotent (sum) | prefix sums | |
| point | range query | Fenwick tree (sum-like) or segment tree (any monoid) | |
| range add | point query | Fenwick/segment tree over the difference array | |
| range add | range sum | two Fenwick trees, or a lazy segment tree | |
| range assign/add | range sum/min/max | lazy segment tree (Lazy Propagation) | |
| range chmin/chmax (+ range add) | range sum | segment tree beats (Segment Tree Beats) | amortized |
| add a line/segment | min/max of lines at a point | Li Chao tree (Li Chao Tree) | |
| none, but queries are offline and “about value”, not position | -th smallest, counts by value in a range | persistent segment tree / wavelet tree (Range Order Statistics) | |
| point, need every historical version | query any past version | persistent segment tree (Dynamic and Persistent Segment Trees) | , new nodes |
| offline, hard to decompose (distinct counts, complex aggregates) | arbitrary range, reorderable | Mo’s algorithm (Mo’s Algorithm) | |
| offline, mixed update/query types | — | sqrt decomposition on queries, or CDQ divide and conquer (CDQ) | problem-dependent, usually or |
| point, values bounded, keys sparse | range query over huge coordinate space | sparse/dynamic segment tree (Dynamic Segment Trees) |
Read the row that matches your (update shape, query shape) pair before reaching for the heaviest tool — most problems are solved by the second or third row, not the exotic ones.