โ† Back to DevBytes

Sparse Tables: Implementation and Time Complexity Analysis

Introduction to Sparse Tables

A Sparse Table is a powerful data structure designed to answer range queries on static arrays โ€” arrays that do not change between queries. It excels at answering queries for idempotent operations such as range minimum, range maximum, range greatest common divisor (GCD), and range bitwise AND/OR. The key advantage of a Sparse Table is its ability to answer each query in O(1) time after an O(n log n) preprocessing step.

The name "sparse" comes from the fact that the precomputed table stores answers for intervals whose lengths are powers of two, rather than every possible interval. This dramatically reduces the space required compared to precomputing answers for every possible query range, which would take O(nยฒ) space.

Why Sparse Tables Matter

Consider a scenario where you have an array of one million integers and you need to answer one million queries asking for the minimum value in a given range. A naive approach would take O(n) per query, resulting in O(nยฒ) total time โ€” far too slow for large inputs. Segment trees can solve this in O(log n) per query, but Sparse Tables go one step further by answering each query in constant time.

Sparse Tables are particularly valuable in competitive programming and systems where:

The Core Idea

The fundamental insight behind Sparse Tables is that any interval [l, r] can be covered by two overlapping intervals whose lengths are powers of two. Specifically, if k = floor(log2(r - l + 1)), then the range [l, r] can be covered by the intervals [l, l + 2^k - 1] and [r - 2^k + 1, r]. Both of these intervals have length 2^k, and together they completely cover [l, r] (with some overlap in the middle).

For idempotent operations, the overlap does not matter because applying the operation to the same element twice produces the same result. This is why Sparse Tables work for min, max, GCD, and bitwise operations, but not for sum or product queries (where overlapping would double-count elements).

Precomputation

We define st[k][i] as the answer for the range [i, i + 2^k - 1] โ€” that is, the range of length 2^k starting at index i. The table is built using dynamic programming:

The number of levels K is floor(log2(n)) + 1, and each level has at most n entries, giving the O(n log n) space and time complexity for preprocessing.

Implementation

Below is a complete C++ implementation of a Sparse Table for range minimum queries. The same structure can be adapted for other idempotent operations by changing the combine function.

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

class SparseTable {
private:
    vector<vector<int>> st;
    vector<int> logTable;
    int n;
    int K;

public:
    SparseTable(const vector<int>& arr) {
        n = arr.size();
        // Precompute log values for O(1) query lookup
        logTable.resize(n + 1);
        logTable[1] = 0;
        for (int i = 2; i <= n; i++) {
            logTable[i] = logTable[i / 2] + 1;
        }

        K = logTable[n] + 1;
        st.assign(K, vector<int>(n));

        // Base case: ranges of length 1
        for (int i = 0; i < n; i++) {
            st[0][i] = arr[i];
        }

        // Build the table for increasing power-of-two lengths
        for (int k = 1; k < K; 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))]);
            }
        }
    }

    // Query the minimum in range [l, r] (0-indexed, inclusive)
    int query(int l, int r) {
        int k = logTable[r - l + 1];
        return min(st[k][l], st[k][r - (1 << k) + 1]);
    }
};

int main() {
    vector<int> arr = {7, 2, 3, 0, 5, 10, 3, 12, 18};
    SparseTable st(arr);

    cout << "Min in [0, 4]: " << st.query(0, 4) << endl;  // Output: 0
    cout << "Min in [4, 7]: " << st.query(4, 7) << endl;  // Output: 3
    cout << "Min in [7, 8]: " << st.query(7, 8) << endl;  // Output: 12

    return 0;
}

Adapting for Other Operations

To adapt the Sparse Table for a different idempotent operation, simply replace the min function in both the build and query methods. Here is an example for range GCD queries:

class SparseTableGCD {
private:
    vector<vector<int>> st;
    vector<int> logTable;
    int n, K;

public:
    SparseTableGCD(const vector<int>& arr) {
        n = arr.size();
        logTable.resize(n + 1);
        logTable[1] = 0;
        for (int i = 2; i <= n; i++) {
            logTable[i] = logTable[i / 2] + 1;
        }

        K = logTable[n] + 1;
        st.assign(K, vector<int>(n));

        for (int i = 0; i < n; i++) {
            st[0][i] = arr[i];
        }

        for (int k = 1; k < K; k++) {
            for (int i = 0; i + (1 << k) <= n; i++) {
                st[k][i] = __gcd(st[k - 1][i],
                                 st[k - 1][i + (1 << (k - 1))]);
            }
        }
    }

    int query(int l, int r) {
        int k = logTable[r - l + 1];
        return __gcd(st[k][l], st[k][r - (1 << k) + 1]);
    }
};

Time Complexity Analysis

Preprocessing

The preprocessing step fills a table of size K ร— n, where K = floor(log2(n)) + 1. Each cell is computed in O(1) time using two previously computed cells. Therefore, the total preprocessing time is:

O(n * K) = O(n * log n)

The precomputation of the log table also takes O(n) time, which is dominated by the O(n log n) table construction.

Query

Each query involves computing k = logTable[r - l + 1] (an O(1) lookup thanks to the precomputed log table) and then combining two values from the table. The total query time is therefore:

O(1)

This constant-time query is the primary reason Sparse Tables are preferred over segment trees for static idempotent range queries.

Space Complexity

The Sparse Table stores K arrays of size n, plus the log table of size n + 1. The total space complexity is:

O(n * log n)

For an array of one million elements, this translates to approximately 20 million integers (since log2(10^6) โ‰ˆ 20), which is about 80 MB of memory โ€” generally acceptable but worth keeping in mind for memory-constrained environments.

Handling Non-Idempotent Operations

For non-idempotent operations like range sum, the standard Sparse Table approach fails because the two overlapping intervals would double-count the elements in the overlap. However, there is a variant that handles sum queries by using O(log n) per query: you decompose the query range into disjoint power-of-two segments and sum them up. This is essentially the same as the binary lifting technique.

long long querySum(int l, int r) {
    long long sum = 0;
    int len = r - l + 1;
    int k = logTable[len];
    for (int i = k; i >= 0; i--) {
        if ((1 << i) <= len) {
            sum += st[i][l];
            l += (1 << i);
            len -= (1 << i);
        }
    }
    return sum;
}

However, for sum queries, a prefix sum array is almost always a better choice since it provides O(n) preprocessing, O(n) space, and O(1) queries. The Sparse Table variant is only useful when you need both idempotent and non-idempotent queries on the same static array.

Best Practices

Comparison with Other Data Structures

To understand when to use a Sparse Table, it helps to compare it with alternatives:

Conclusion

Sparse Tables are an elegant and highly efficient data structure for answering static range queries with idempotent operations. By precomputing answers for all power-of-two-length intervals, they achieve O(1) query time after O(n log n) preprocessing, making them ideal for scenarios involving massive numbers of queries on immutable data. While they are limited to idempotent operations and cannot handle updates, their simplicity, speed, and predictable performance make them a valuable tool in any developer's or competitive programmer's toolkit. Understanding when to reach for a Sparse Table versus a segment tree or prefix sum array is a key skill for writing efficient range-query code.

๐Ÿ›  Tools from DevBytes

Inventory Tracker Pro โ€” Excel inventory system, low-stock alerts ยท $19
AI Dev Kit for Mac โ€” local AI dev environment templates ยท $9.99
KeyMapper for Mac โ€” custom keyboard shortcut toolkit ยท $7.99

โ† Back to all articles