Sparse Tables & Range Queries

Sparse Table ও Range query

Read: ~35 min Advanced 6 practice problems Live code runner

1. The Static-Data Niche

When the array never changes and we only ask range minimum / maximum / gcd queries, we can do O(1) per query with O(n log n) preprocessing using a sparse table. This is faster than segment tree's O(log n) per query.

update না থাকলে segment tree-এর চেয়েও দ্রুত — sparse table O(1) query, কিন্তু O(n log n) preprocessing লাগে।

2. Idempotent Functions

A function op is idempotent if op(x, x) = x. Examples: min, max, gcd, bitwise AND, bitwise OR. For these, overlapping ranges in a query don't double-count — we can answer with two precomputed power-of-2 windows.

Why idempotent matters For RMQ on [l, r] of length len, let k = ⌊log₂ len⌋. Then [l, l+2ᵏ−1] and [r−2ᵏ+1, r] cover [l, r] (overlapping in the middle, but min/max don't care about overlap).
sum কাজ করবে না (overlap ডবল গণনা হবে), কিন্তু min/max/gcd করবে।

3. Building & Querying a Sparse Table

st[k][i] = min of a[i .. i + 2ᵏ − 1]. Recurrence: st[k][i] = min(st[k−1][i], st[k−1][i + 2^(k−1)]). Total O(n log n) time and space.

sparse_min.cpp
#include <bits/stdc++.h>
using namespace std;

struct SparseMin {
    vector<vector<int>> st;
    vector<int> lg;

    SparseMin(vector<int>& a) {
        int n = a.size();
        int K = log2(n) + 1;
        st.assign(K, vector<int>(n));
        st[0] = a;
        for (int k = 1; (1 << k) <= n; k++)
            for (int i = 0; i + (1 << k) <= n; i++)
                st[k][i] = min(st[k-1][i], st[k-1][i + (1 << (k-1))]);

        lg.assign(n + 1, 0);
        for (int i = 2; i <= n; i++) lg[i] = lg[i / 2] + 1;
    }

    int query(int l, int r) {                // inclusive [l, r]
        int k = lg[r - l + 1];
        return min(st[k][l], st[k][r - (1 << k) + 1]);
    }
};

int main() {
    vector<int> a = {7, 2, 3, 0, 5, 10, 3, 12, 18};
    SparseMin sm(a);
    cout << "min[2..7] = " << sm.query(2, 7) << "\n";          // 0
    cout << "min[5..8] = " << sm.query(5, 8);                  // 3
}

4. Square-Root Decomposition

Split the array into blocks of size √n. Pre-aggregate each block. A range query touches at most O(√n) full blocks + O(√n) edge elements → O(√n) per query. Updates are O(1) for the element + O(√n) for the block aggregate → O(√n).

√n decomposition হলো segment tree-এর সরল বিকল্প। ছোট কোড, একটু ধীর — কিন্তু আশ্চর্যজনকভাবে অনেক ICPC সমস্যায় সফল।

5. Mo's Algorithm — Offline Queries in O((n+q)·√n)

For offline range queries on static data, Mo's algorithm sorts queries by (block, r) and walks two pointers — adding / removing elements — to amortise to O((n + q)·√n). Used for "count distinct in [l, r]", "frequency-of-mode", etc.

Online vs Offline

  • Online: queries arrive one at a time, must be answered immediately
  • Offline: all queries known up front, can be reordered freely

Mo's pre-conditions

  • Static array
  • Add / remove at the ends in O(1) (or O(log n))
  • All queries known upfront

6. Practice Problems

  1. Range minimum query on the array [3, 1, 4, 1, 5, 9, 2, 6] for [2..6].
    Sparse table দিয়ে min[2..6] বের করুন।
    ✨ Show Answer (উত্তর দেখুন)

    Answer: values are 4, 1, 5, 9, 2 → min = 1. With sparse table: len = 5, k = 2, min(st[2][2], st[2][3]) = min(min(4,1,5,9), min(1,5,9,2)) = min(1, 1) = 1.

  2. Range GCD query — modify the sparse table to compute gcd.
    Range GCD query — sparse table-এ পরিবর্তন।
    ✨ Show Answer (উত্তর দেখুন)

    Change: replace min(...) with __gcd(...). GCD is idempotent: gcd(x, x) = x — overlap is fine.

  3. Why does sum NOT work on a sparse table?
    Sum কেন sparse table-এ চলে না?
    ✨ Show Answer (উত্তর দেখুন)

    Answer: the two overlapping power-of-2 windows would double-count elements in the overlap. Sum is not idempotent (sum(x, x) = 2x, not x). For sum on a static array, use a prefix-sum array — O(1) query, no overlap issue.

  4. Static range mode (most frequent value) using Mo's algorithm — sketch.
    Static range mode — Mo's algorithm দিয়ে।
    ✨ Show Answer (উত্তর দেখুন)

    Sketch: sort queries by (l / √n, r). Maintain cnt[value] and freq[count]. On add: cnt[v]++; freq[cnt[v] − 1]−−; freq[cnt[v]]++; track maxCount. On remove: reverse. After processing each window, mode count = maxCount. O((n+q)√n).

  5. Count distinct values in [l, r] for offline queries — outline.
    Range distinct count — Mo's algorithm।
    ✨ Show Answer (উত্তর দেখুন)

    Outline: Mo's, with cnt[v]++; if cnt[v] == 1 → distinct++. On remove: cnt[v]−−; if cnt[v] == 0 → distinct−−. Answer is distinct.

  6. If the array is dynamic (point updates allowed), would you still use a sparse table? Why or why not?
    Update থাকলে sparse table কেন কাজ করবে না?
    ✨ Show Answer (উত্তর দেখুন)

    Answer: a point update at index i invalidates O(log n) entries per level — total O(log² n) entries to fix per update, but in the worst case across the table this becomes O(n log n) per update. Use a segment tree instead — O(log n) per update.

Summary — Module 24

Sparse table: O(n log n) preprocess + O(1) query for idempotent ops on static data. Square-root decomposition: simpler O(√n) range structures with updates. Mo's algorithm: offline queries in O((n + q)√n). Each tool fits a different update / query pattern — choose by data movement, not by complexity alone.

Static + idempotent → sparse table। সরল কিন্তু update needed → √n decomposition। Offline queries → Mo's algorithm।

Next Module → Skip Lists & Treaps — randomized balanced structures।