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).

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]);
    }
};

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

OperationStructureQuery
sum, XOR (invertible)prefix sums
min, max, gcd, AND, ORsparse table, or RMQ (Farach-Colton-Bender, see RMQ)
any associativedisjoint sparse table, sqrt tree
associative, low memorysegment tree
-th smallest, counts by valuewavelet matrix, persistent segment tree

Choosing a range-query structure in general

The full (update, query) matrix — the decision this whole chapter answers:

UpdatesQueriesStructureCost
nonerange query, idempotent opsparse table / disjoint sparse table / sqrt tree
nonerange query, non-idempotent (sum)prefix sums
pointrange queryFenwick tree (sum-like) or segment tree (any monoid)
range addpoint queryFenwick/segment tree over the difference array
range addrange sumtwo Fenwick trees, or a lazy segment tree
range assign/addrange sum/min/maxlazy segment tree (Lazy Propagation)
range chmin/chmax (+ range add)range sumsegment tree beats (Segment Tree Beats)amortized
add a line/segmentmin/max of lines at a pointLi Chao tree (Li Chao Tree)
none, but queries are offline and “about value”, not position-th smallest, counts by value in a rangepersistent segment tree / wavelet tree (Range Order Statistics)
point, need every historical versionquery any past versionpersistent segment tree (Dynamic and Persistent Segment Trees), new nodes
offline, hard to decompose (distinct counts, complex aggregates)arbitrary range, reorderableMo’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 sparserange query over huge coordinate spacesparse/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.