DSA DSA In Depth
0%
lesson sparse-tablermqrange-queries

Disjoint & Standard Sparse Tables: O(1) Idempotent Range Minimum Query (RMQ)

7 min read Module 3.3: Range Query & Sublinear Decomposition Structures Updated Oct 23, 2026
📄 Cheat Sheet
Sparse Table & Range Minimum Query (RMQ) in O(1) Time
Errichto Algorithms Sparse Table & Range Minimum Query (RMQ) in O(1) Time Watch on YouTube ↗

Clear geometric visualization of how overlapping power-of-two intervals cover any query range [L, R] in O(1) time for idempotent operations.

0:00 / --:--

1. The Algebra of Range Queries: Semigroups, Monoids, Groups, and Idempotence

Before selecting a range query structure, classify the binary operator $\oplus: S \times S \to S$:

  • Group (Invertible Associative Operation) — e.g., $+, \oplus(\text{XOR}), \times \pmod p$. Because every element has an inverse $a^{-1}$, any static range query $A[L \dots R]$ reduces to a 1D prefix array $P[i] = P[i-1] \oplus A[i]$ via $P[R] \oplus P[L-1]^{-1}$ in $O(N)$ build and $O(1)$ query time.
  • Idempotent Semigroup (Band) — e.g., $\min, \max, \gcd, \text{lcm}, \&, |$. These operations have no inverse (knowing $\min(A[0..R]) = 2$ and $\min(A[0..L-1]) = 2$ tells you nothing about $\min(A[L..R])$), so prefix arrays fail. However, they satisfy idempotence: $x \oplus x = x$. Consequently, if two sub-intervals $I_1, I_2$ satisfy $I_1 \cup I_2 = [L, R]$, then $\text{ans}(I_1) \oplus \text{ans}(I_2) = \text{ans}([L, R])$ even when $I_1 \cap I_2 \neq \emptyset$.
  • General Semigroup / Monoid (Non-Invertible, Non-Idempotent) — e.g., $2 \times 2$ matrix multiplication, polynomial hashing, maximum subarray sum tuple, string concatenation. Neither prefix inversion nor overlapping intervals work; we must partition $[L, R]$ into strictly disjoint intervals.
Standard Sparse Table Coverage Invariant: For any interval $[L, R]$ of length $\text{len} = R - L + 1$, let $k = \lfloor \log_2(\text{len}) \rfloor$, so $2^k \le \text{len} < 2^{k+1}$. Then the left interval $[L, L + 2^k - 1]$ and the right interval $[R - 2^k + 1, R]$ both lie entirely inside $[L, R]$ and satisfy $(L + 2^k - 1) \ge (R - 2^k + 1)$, meaning their union is exactly $[L, R]$: $$\bigoplus_{i=L}^{R} A[i] = \text{st}[k][L] \oplus \text{st}[k][R - 2^k + 1]$$

2. Standard Sparse Table Construction & Disjoint Sparse Table (DST)

The Standard Sparse Table builds a 2D table $\text{st}[k][i]$ of size $(\lfloor \log_2 N \rfloor + 1) \times N$, where $\text{st}[k][i]$ stores the range aggregate over $[i, i + 2^k - 1]$. Level $0$ is just $\text{st}[0][i] = A[i]$, and each subsequent level is built by combining two adjacent intervals of length $2^{k-1}$:

$$\text{st}[k][i] = \text{st}[k-1][i] \oplus \text{st}[k-1][i + 2^{k-1}]$$

In contrast, a Disjoint Sparse Table (DST) pads $N$ to a power of two $2^H$ and considers blocks of size $2^{H - h}$ at depth $h \in [0, H-1]$. Around the midpoint $M$ of each block, DST computes suffixes moving left from $M-1$ down to the block start, and prefixes moving right from $M$ up to the block end. For any query $L < R$, let $b = \lfloor \log_2(L \oplus R) \rfloor$ be the most significant bit where $L$ and $R$ differ. At level $h = H - 1 - b$, indices $L$ and $R$ fall into the left and right halves of the same block separated by midpoint $M$, so $\text{dst}[h][L] \oplus \text{dst}[h][R]$ combines $[L, M-1]$ and $[M, R]$ with zero overlap in $O(1)$ time!

1. Standard Sparse Table (Idempotent: min, max, gcd) Query [L=2, R=8], len = 7, k = floor(log2(7)) = 2 (2^k = 4) st[2][2] covers [2..5] st[2][5] covers [5..8] Overlap at index 5 is safe because min(x, x) = x 2. Disjoint Sparse Table (Any Semigroup: Matrix, Sum, Prod) Query [L=2, R=6], msb(2 ^ 6) = msb(4) = bit 2 -> Midpoint M = 4 Midpoint M=4 dst[h][2] dst[h][6] [2..3] and [4..6] are strictly disjoint: zero double-counting!
flowchart LR subgraph Level0["k = 0 (len = 1)"] A0["[0..0]"] A1["[1..1]"] A2["[2..2]"] A3["[3..3]"] end subgraph Level1["k = 1 (len = 2)"] B0["[0..1]"] B1["[1..2]"] B2["[2..3]"] end subgraph Level2["k = 2 (len = 4)"] C0["[0..3]"] end A0 & A1 --> B0 A1 & A2 --> B1 A2 & A3 --> B2 B0 & B2 --> C0
Hardware & Cache Reality — Row-Major Layout & Hardware CLZ: Always index your Sparse Table as st[k][i] (level major) rather than st[i][k]. During construction, st[k][i] = min(st[k-1][i], st[k-1][i + (1 << (k-1))]) streams sequentially across two contiguous rows of st[k-1], allowing the CPU hardware prefetcher and AVX2 _mm256_min_epi32 vector instructions to build 8 cells per clock cycle. During queries, never call floating-point Math.log2(len); use integer bit intrinsics 31 - Math.clz32(len) (or __builtin_clz in C++, which compiles to a single 1-cycle LZCNT / BSR instruction).
Sparse Table O(1) Idempotent RMQ & Euler-Tour / Binary-Lifting LCA
O(N log N) Build · O(1) RMQ & LCA

See why idempotent operations (min, max, gcd) answer range queries in O(1) time via two overlapping power-of-two blocks st[k][L] and st[k][R − 2^k + 1], and how LCA(u, v) reduces to RMQ on the Euler Tour depth array.

Step 1 / 8
Custom Input:
Timeline

Invariant & State

Step 1 (Compute Scale k): For query [L=1, R=6], length = R − L + 1 = 6. Largest power of 2 ≤ 6 is 2^k = 4 (k = ⌊log₂ 6⌋ = 2).
🌳 Live 2D Hierarchical Tree Topology — Sparse Table O(1) Idempotent RMQ & Euler-Tour / Binary-Lifting LCA Visited/Active Pivot/Root Resolved
A[0]=7 [0] L = 1 A[1]=2 [1] A[2]=3 [2] A[3]=0 [3] A[4]=5 [4] A[5]=… [5] R = 6 (l A[6]=3 [6] A[7]=… [7]
[0]A[0]=7
[1]A[1]=2L = 1
[2]A[2]=3
[3]A[3]=0
[4]A[4]=5
[5]A[5]=10
[6]A[6]=3R = 6 (len = 6, k = ⌊log₂ 6⌋ = 2)
[7]A[7]=12
Query Interval [L, R]
[1, 6] (len = 6)
Scale k = ⌊log₂ len⌋
k = 2 (2^k = 4)
Overlapping Lookup
min(st[2][1], st[2][3]) = 0
Query Time
O(1) (2 Table Reads)
[Step 1] Precomputed lg[6] = 2 in O(1). Two blocks of length 2² = 4 will completely cover [1..6]: prefix [1..4] and suffix [3..6].

3. Production Implementation: Standard RMQ & Disjoint Sparse Table

/**
 * 1. Standard Sparse Table for O(1) Idempotent Range Minimum Query (RMQ)
 * Build: O(N log N) time & space | Query: O(1) time (2 table lookups + 1 clz32)
 */
class SparseTableRMQ {
  constructor(arr) {
    this.n = arr.length;
    this.maxK = 31 - Math.clz32(this.n);
    // Flat Float64Array / Int32Array per level for L1/L2 cache locality
    this.st = Array.from({ length: this.maxK + 1 }, () => new Int32Array(this.n));
    for (let i = 0; i < this.n; i++) this.st[0][i] = arr[i];

    for (let k = 1; k <= this.maxK; k++) {
      const half = 1 << (k - 1);
      const limit = this.n - (1 << k);
      const prev = this.st[k - 1];
      const curr = this.st[k];
      for (let i = 0; i <= limit; i++) {
        const a = prev[i], b = prev[i + half];
        curr[i] = a < b ? a : b;
      }
    }
  }

  query(l, r) {
    const k = 31 - Math.clz32(r - l + 1);
    const row = this.st[k];
    const a = row[l], b = row[r - (1 << k) + 1];
    return a < b ? a : b;
  }
}

/**
 * 2. Disjoint Sparse Table (DST) for O(1) Non-Idempotent Semigroup Queries
 * Supports any associative combine(a, b) (e.g., modular product, matrix mult)
 */
class DisjointSparseTable {
  constructor(arr, combine) {
    this.combine = combine;
    this.levels = Math.max(1, 32 - Math.clz32(arr.length - 1));
    this.size = 1 << this.levels;
    this.table = Array.from({ length: this.levels }, () => new Array(this.size));

    for (let h = 0; h < this.levels; h++) {
      const blockLen = 1 << (this.levels - h);
      const half = blockLen >> 1;
      for (let start = 0; start < arr.length; start += blockLen) {
        const mid = Math.min(start + half, arr.length);
        const end = Math.min(start + blockLen, arr.length);
        // Suffixes extending left from mid - 1 down to start
        if (mid - 1 < arr.length) {
          this.table[h][mid - 1] = arr[mid - 1];
          for (let i = mid - 2; i >= start; i--) {
            this.table[h][i] = combine(arr[i], this.table[h][i + 1]);
          }
        }
        // Prefixes extending right from mid up to end - 1
        if (mid < arr.length) {
          this.table[h][mid] = arr[mid];
          for (let i = mid + 1; i < end; i++) {
            this.table[h][i] = combine(this.table[h][i - 1], arr[i]);
          }
        }
      }
    }
  }

  query(l, r) {
    if (l === r) return this.table[this.levels - 1][l];
    const msbDiff = 31 - Math.clz32(l ^ r);
    const h = this.levels - 1 - msbDiff;
    return this.combine(this.table[h][l], this.table[h][r]);
  }
}

4. Complexity Matrix

Structure Required Algebraic Property Build Time Space Complexity Range Query Time
Prefix Array Invertible Group ($+, \oplus$) $\Theta(N)$ $\Theta(N)$ $\Theta(1)$
Standard Sparse Table Idempotent Semigroup ($\min, \max, \gcd$) $\Theta(N \log N)$ $\Theta(N \log N)$ $\Theta(1)$
Disjoint Sparse Table (DST) Any Associative Semigroup $\Theta(N \log N)$ $\Theta(N \log N)$ $\Theta(1)$
Block RMQ + Four Russians ($\pm 1$ RMQ) Idempotent RMQ $\Theta(N)$ $\Theta(N)$ $\Theta(1)$
Common Pitfall — Using Standard Sparse Table on Non-Idempotent Operations: Never use the overlapping $O(1)$ query formula $\text{st}[k][L] \oplus \text{st}[k][R - 2^k + 1]$ for range sum, XOR, or GCD-count! Unless $R - L + 1$ happens to be an exact power of two, the middle indices $[R - 2^k + 1, L + 2^k - 1]$ are included in both lookups and will be counted twice. Either use a Disjoint Sparse Table ($O(1)$) or iterate over the set bits of $R - L + 1$ in $O(\log N)$ time.
Check yourself Question 1 of 4
Concept Check Algebraic Requirement for O(1) Overlapping Sparse Table Queries recall

A Standard Sparse Table answers a range query on $[L, R]$ in $O(1)$ time by computing $\text{st}[k][L] \oplus \text{st}[k][R - 2^k + 1]$ where $k = \lfloor \log_2(R - L + 1) \rfloor$. Which algebraic property of the binary operation $\oplus$ is required for this overlapping lookup to be valid?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

Why is `st[k][i]` (level-major) significantly faster to construct and more amenable to SIMD vectorization than `st[i][k]`?

Key Takeaways

Standard Sparse Tables exploit idempotence (x op x = x) to cover any interval [L, R] using two overlapping blocks of size 2^k where k = floor(log2(R - L + 1)) in O(1) time.
Disjoint Sparse Tables (DST) generalize O(1) static queries to any associative semigroup (matrices, modular products, max subarray tuples) by precomputing suffixes and prefixes radiating from block midpoints.
In a DST, the exact level h where L and R straddle the midpoint is found in O(1) time via the most significant bit of L ^ R (31 - Math.clz32(L ^ R)).
Always store Sparse Tables in level-major order st[k][i] so construction streams sequentially across cache lines and enables SIMD vectorization.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue