Sparse table menyimpan jawaban untuk setiap range yang panjangnya adalah pangkat dua. Dengan prekalkulasi log dan strategi overlap, query O(1) untuk operasi idempotent — yaitu operasi yang hasilnya tidak berubah bila suatu elemen dihitung dua kali: min(x, x) = x, max(x, x) = x, gcd(x, x) = x. Sedangkan operasi sum, xor, product non-idempotent — menghitung elemen dua kali menghasilkan jawaban salah. Untuk operasi non-idempotent, sparse table standar tidak bisa dipakai karena strategi overlap-then-merge menggandakan elemen. Solusinya: disjoint sparse table yang menjaga partisi bebas overlap. Pada Chapter ini kita membedah keduanya: sparse table idempotent klasik dan disjoint sparse table untuk operasi non-idempotent.
Aturan: Idempotent vs Non-idempotent #
Operasi ⊕ idempotent bila ⊕(x, x) = x untuk semua x.
| Operasi | Idempotent? | Sparse table standar? |
|---|---|---|
min, max | Ya | Ya, O(1) query |
gcd, lcm | Ya | Ya, O(1) query |
Bitwise &, | | Ya | Ya, O(1) query |
sum, xor (non-trivial) | Tidak | Tidak, gunakan disjoint sparse table |
product (matrix) | Tidak | Disjoint sparse table |
Sparse table standar memakai trik: untuk query [l, r] dengan len = r - l + 1, ambil k = floor(log2(len)). Jawaban = merge(table[k][l], table[k][r - 2^k + 1]). Kedua segmen [l, l+2^k-1] dan [r-2^k+1, r] overlap tetapi karena operasi idempotent, overlap tidak menghasilkan kesalahan.
Mekanisme: Build Level-by-Level #
Sparse table level k menyimpan jawaban untuk range [i, i + 2^k - 1] di table[k][i]. Build dari level 0 (range length 1) naik ke level K = floor(log2(n)).
table[0][i] = arr[i].table[k][i] = merge(table[k-1][i], table[k-1][i + 2^(k-1)])— gabung dua segmen berurutan masing-masing panjang2^(k-1).
Query [l, r]:
k = log[r - l + 1].result = merge(table[k][l], table[k][r - 2^k + 1]).
Kedua segmen memiliki panjang 2^k ≥ setengah dari len, jadi union mereka mencakup [l, r] sepenuhnya (overlap di tengah).
Implementasi Sparse Table (Min) dan Disjoint Sparse Table (Sum) #
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
class SparseTable {
public:
explicit SparseTable(const std::vector<int>& arr) {
n_ = arr.size();
int K = std::max(1, (int)std::log2((double)n_) + 1);
table_.assign(K, std::vector<int>(n_));
for (int i = 0; i < n_; ++i) table_[0][i] = arr[i];
for (int k = 1; (1 << k) <= n_; ++k) {
for (int i = 0; i + (1 << k) - 1 < n_; ++i) {
table_[k][i] = std::min(table_[k-1][i],
table_[k-1][i + (1 << (k-1))]);
}
}
log_.assign(n_ + 1, 0);
for (int i = 2; i <= n_; ++i) log_[i] = log_[i / 2] + 1;
}
int query(int l, int r) const {
int k = log_[r - l + 1];
return std::min(table_[k][l], table_[k][r - (1 << k) + 1]);
}
private:
int n_;
std::vector<std::vector<int>> table_;
std::vector<int> log_;
};
class DisjointSparseTable {
public:
explicit DisjointSparseTable(const std::vector<long long>& arr) {
n_ = arr.size();
int K = 1;
while ((1 << K) < 2 * n_) ++K;
K = std::max(K, 2);
table_.assign(K, std::vector<long long>(n_, 0));
for (int i = 0; i < n_; ++i) table_[0][i] = arr[i];
for (int k = 1; k < K; ++k) {
int step = 1 << (k - 1);
for (int mid = step; mid < n_; mid += step * 2) {
int L = std::max(0, mid - step);
int R = std::min(n_ - 1, mid + step - 1);
// Left half [L, mid-1]: table_[k][i] = sum(arr[i..mid-1])
table_[k][mid - 1] = arr[mid - 1];
for (int i = mid - 2; i >= L; --i)
table_[k][i] = arr[i] + table_[k][i + 1];
// Right half [mid, R]: table_[k][i] = sum(arr[mid..i])
table_[k][mid] = arr[mid];
for (int i = mid + 1; i <= R; ++i)
table_[k][i] = table_[k][i - 1] + arr[i];
}
}
}
long long query(int l, int r) const {
if (l == r) return table_[0][l];
int k = 32 - __builtin_clz((unsigned)(l ^ r));
return table_[k][l] + table_[k][r];
}
private:
int n_;
std::vector<std::vector<long long>> table_;
};
int main() {
std::vector<int> arr = {7, 2, 3, 1, 5, 9, 4, 6};
SparseTable st(arr);
std::cout << "SparseTable (min):\n";
std::cout << " min[1,5] = " << st.query(1, 5) << '\n';
std::cout << " min[0,7] = " << st.query(0, 7) << '\n';
std::cout << " min[3,3] = " << st.query(3, 3) << '\n';
std::vector<long long> arr2 = {1, 2, 3, 4, 5, 6, 7, 8};
DisjointSparseTable dst(arr2);
std::cout << "\nDisjointSparseTable (sum):\n";
std::cout << " sum[1,5] = " << dst.query(1, 5) << '\n';
std::cout << " sum[0,7] = " << dst.query(0, 7) << '\n';
std::cout << " sum[2,4] = " << dst.query(2, 4) << '\n';
std::cout << " sum[3,3] = " << dst.query(3, 3) << '\n';
return 0;
}Output yang Dihasilkan:
SparseTable (min):
min[1,5] = 1
min[0,7] = 1
min[3,3] = 1
DisjointSparseTable (sum):
sum[1,5] = 20
sum[0,7] = 36
sum[2,4] = 12
sum[3,3] = 4Bedah Kode Baris-per-Baris #
table_.assign(K, std::vector<int>(n_))(SparseTable): matriksK × ndi manaK = floor(log2(n)) + 1. Setiap baris adalah level. Total memoriO(n log n).table_[0][i] = arr[i]: level 0 = elemen array itu sendiri (range length 1).table_[k][i] = std::min(table_[k-1][i], table_[k-1][i + (1 << (k-1))]): gabung dua segmen berdekatan masing-masing panjang2^(k-1)→ segmen panjang2^k.log_[]array: precomputelog[i]untuk semuai ∈ [0, n]agar query O(1) tanpa log() call.log[i] = log[i/2] + 1.query(l, r): ambilk = log_[r-l+1](level terbesar yang2^k ≤ len). Hasil =min(table_[k][l], table_[k][r - 2^k + 1]). Kedua segmen[l, l+2^k-1]dan[r-2^k+1, r]overlap di tengah — karenaminidempoten, tidak masalah.DisjointSparseTablelevel 0:table_[0][i] = arr[i], sama seperti sparse table biasa.- Level k (k ≥ 1):
step = 2^(k-1). Loopmid = step, 3*step, 5*step, ...(midpoint pada setiap blok ukuran2*step = 2^k). Untuk setiap midpoint, isi separuh kiri[L, mid-1]dengan suffix-sum ke midpoint, dan separuh kanan[mid, R]dengan prefix-sum dari midpoint. table_[k][i]di separuh kiri = sum(arr[i..mid-1]): dihitung dari kanan ke kiri.table_[k][mid-1] = arr[mid-1], lalutable_[k][i] = arr[i] + table_[k][i+1].table_[k][i]di separuh kanan = sum(arr[mid..i]): dihitung dari kiri ke kanan.table_[k][mid] = arr[mid], lalutable_[k][i] = table_[k][i-1] + arr[i].query(l, r): bilal == r, return elemen tunggal. Bila tidak, hitungk = 32 - __builtin_clz(l ^ r).l ^ rmemberi posisi bit berbeda tertinggi —k= level di manaldanrberada di separuh berbeda dari satu blok. Returntable_[k][l] + table_[k][r](suffix sum darilke midpoint + prefix sum dari midpoint ker, tidak ada overlap).- Verifikasi:
- SparseTable:
arr = [7,2,3,1,5,9,4,6].min[1,5] = min(2,3,1,5,9) = 1.min[0,7] = 1.min[3,3] = 1. ✓ - DisjointSparseTable:
arr2 = [1,2,3,4,5,6,7,8].sum[1,5] = 2+3+4+5+6 = 20.sum[0,7] = 36.sum[2,4] = 3+4+5 = 12.sum[3,3] = 4. ✓
- SparseTable:
Kompleksitas dan Perbandingan #
| Struktur | Build | Query | Update | Memori | Operasi |
|---|---|---|---|---|---|
| Sparse table (idempotent) | O(n log n) | O(1) | Tidak | O(n log n) | min, max, gcd, bitwise |
| Disjoint sparse table | O(n log n) | O(1) | Tidak | O(n log n) | sum, xor, product |
| Fenwick tree | O(n) | O(log n) | O(log n) | O(n) | sum, prefix-friendly |
| Segment tree | O(n) | O(log n) | O(log n) | O(4n) | monoid apapun |
Ringkasan #
- Sparse table menyimpan jawaban untuk setiap range berukuran pangkat dua.
- Standar sparse table: untuk operasi idempotent (
min,max,gcd,&,|). Query O(1), build O(n log n), memori O(n log n). - Disjoint sparse table: untuk operasi non-idempotent (
sum,xor,product). Level k menyimpan prefix/suffix sum terhadap midpoint blok ukuran2^k. Query O(1). - Sparse table tidak mendukung update — array harus statis.
- Untuk dynamic + range query, gunakan Fenwick atau segment tree.
- Trade-off: sparse table O(1) query vs segment tree O(log n) query, tetapi sparse table O(n log n) memori dan build vs segment tree O(n).