Bayangkan kita punya array A berisi n elemen dan harus menjawab banyak query berulang: “berapa jumlah elemen pada range [l, r]?", “berapa nilai minimum pada [l, r]?". Solusi naif: iterasi A[l..r] setiap query, O(n) per query. Untuk q query, total O(n·q). Bila n = q = 100rb, total 10 miliar operasi — terlalu lambat. Segment tree menyelesaikan masalah ini dengan praproses O(n) dan menjawab setiap range query dalam O(log n). Bonus: update satu elemen juga O(log n), jauh lebih cepat dari prefix sum array yang O(n) per update. Pada Chapter ini kita akan mengimplementasikan segment tree rekursif dan iteratif, mendukung operasi sum, min, dan max, serta memahami kapan segment tree unggul dibanding alternatif.
Inti Segment Tree #
Segment tree menyimpan gabungan (aggregate) dari setiap segmen array dalam node tree. Root menyimpan aggregate seluruh array [0, n-1]. Anak kiri menyimpan aggregate [0, (n-1)/2], anak kanan menyimpan [(n-1)/2 + 1, n-1], dan seterusnya sampai leaf yang masing-masing menyimpan satu elemen.
Pada tree di atas, array A = [1, 2, 3, 3, 4, 8]. Root menyimpan sum total 21. Untuk query sum [2, 5] = 3+3+4+8 = 18, segment tree menggabungkan node [2,2] (sum=3) dan [3,5] (sum=15) → 18.
Build, Query, dan Update #
Build (O(n)) #
Bangun segment tree dari array secara rekursif post-order:
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
class SegmentTree {
public:
explicit SegmentTree(const std::vector<int>& arr) {
n_ = arr.size();
tree_.assign(4 * n_, 0);
build(arr, 1, 0, n_ - 1);
}
int sum(int l, int r) const { return query_sum(1, 0, n_ - 1, l, r); }
void update(int idx, int val) { update_rec(1, 0, n_ - 1, idx, val); }
private:
int n_;
std::vector<int> tree_;
void build(const std::vector<int>& arr, int node, int lo, int hi) {
if (lo == hi) { tree_[node] = arr[lo]; return; }
int mid = (lo + hi) / 2;
build(arr, 2*node, lo, mid);
build(arr, 2*node + 1, mid+1, hi);
tree_[node] = tree_[2*node] + tree_[2*node + 1];
}
int query_sum(int node, int lo, int hi, int ql, int qr) const {
if (qr < lo || hi < ql) return 0; // no overlap
if (ql <= lo && hi <= qr) return tree_[node]; // total overlap
int mid = (lo + hi) / 2;
return query_sum(2*node, lo, mid, ql, qr)
+ query_sum(2*node + 1, mid+1, hi, ql, qr);
}
void update_rec(int node, int lo, int hi, int idx, int val) {
if (lo == hi) { tree_[node] = val; return; }
int mid = (lo + hi) / 2;
if (idx <= mid) update_rec(2*node, lo, mid, idx, val);
else update_rec(2*node + 1, mid+1, hi, idx, val);
tree_[node] = tree_[2*node] + tree_[2*node + 1];
}
};
int main() {
std::vector<int> arr = {1, 2, 3, 3, 4, 8};
SegmentTree st(arr);
std::cout << "sum [0,5] = " << st.sum(0, 5) << '\n';
std::cout << "sum [2,5] = " << st.sum(2, 5) << '\n';
std::cout << "sum [1,3] = " << st.sum(1, 3) << '\n';
st.update(3, 10); // arr[3] = 10 (sebelumnya 3)
std::cout << "\nSetelah update arr[3] = 10:\n";
std::cout << "sum [0,5] = " << st.sum(0, 5) << '\n';
std::cout << "sum [2,5] = " << st.sum(2, 5) << '\n';
return 0;
}Output yang Dihasilkan:
sum [0,5] = 21
sum [2,5] = 18
sum [1,3] = 8
Setelah update arr[3] = 10:
sum [0,5] = 28
sum [2,5] = 25tree_.assign(4 * n_, 0): segment tree memerlukan array berukuran4n(aman) atau2 × next_pow2(n)(optimal). Ukuran4nmelindungi dari edge casenganjil.build: post-order rekursif. Bilalo == hi(leaf), assign elemen array. Bila bukan, rekursi ke anak kiri[lo, mid]dan anak kanan[mid+1, hi], lalu gabung dengan operator (sum =left + right).query_sum: tiga kasus overlap:- No overlap (
qr < lo || hi < ql): return 0 (identitas untuk sum). - Total overlap (
ql <= lo && hi <= qr): returntree_[node]. - Partial overlap: rekursi ke kedua anak dan gabung hasilnya.
- No overlap (
update_rec: turun ke leaf yang sesuai denganidx. Setelah update leaf, naik sambil update parent:tree_[parent] = tree_[left] + tree_[right].- Hasil sum
[2,5]: 3 + 3 + 4 + 8 = 18. Sesuai ekspektasi. Setelah updatearr[3] = 10, sum[2,5]= 3 + 10 + 4 + 8 = 25.
Iterative Segment Tree (Lebih Cepat) #
Versi iteratif menghindari overhead rekursi, ~2-3× lebih cepat untuk n besar. Memakai konvensi array 1-indexed dengan leaf di posisi [n, 2n-1].
#include <iostream>
#include <vector>
class IterSegmentTree {
public:
explicit IterSegmentTree(const std::vector<int>& arr) {
n_ = arr.size();
tree_.assign(2 * n_, 0);
for (int i = 0; i < n_; ++i) tree_[n_ + i] = arr[i];
for (int i = n_ - 1; i >= 1; --i) tree_[i] = tree_[2*i] + tree_[2*i + 1];
}
void update(int idx, int val) {
idx += n_;
tree_[idx] = val;
for (idx /= 2; idx >= 1; idx /= 2) {
tree_[idx] = tree_[2*idx] + tree_[2*idx + 1];
}
}
int sum(int l, int r) const {
l += n_; r += n_;
int s = 0;
while (l <= r) {
if (l % 2 == 1) s += tree_[l++];
if (r % 2 == 0) s += tree_[r--];
l /= 2; r /= 2;
}
return s;
}
private:
int n_;
std::vector<int> tree_;
};
int main() {
std::vector<int> arr = {1, 2, 3, 3, 4, 8};
IterSegmentTree st(arr);
std::cout << "Iter sum [0,5] = " << st.sum(0, 5) << '\n';
std::cout << "Iter sum [2,5] = " << st.sum(2, 5) << '\n';
st.update(3, 10);
std::cout << "Setelah update arr[3]=10: sum [0,5] = " << st.sum(0, 5) << '\n';
return 0;
}Output yang Dihasilkan:
Iter sum [0,5] = 21
Iter sum [2,5] = 18
Setelah update arr[3]=10: sum [0,5] = 28- Leaf di
[n, 2n-1]: konvensi iterative tree meletakkan elemen array pada indeksnsampai2n-1. Indeksnadalah leaf ke-0,n+1leaf ke-1, dst. - Build dari bawah ke atas: loop
i = n-1hingga1, setiap node adalah gabungan dua anak2idan2i+1. updateiteratif: setelah update leaf, naik denganidx /= 2sambil menggabung ulang parent.sumiteratif: dua pointerldanrdari leaf ke root. Bilalganjil → node tersebut adalah anak kanan parent → tambahkan ke hasil lalul++. Bilargenap → anak kiri parent → tambahkan ke hasil lalur--. Kedua pointer naik dengan/= 2.- Hasil sesuai versi rekursif — keduanya menghasilkan output identik. Iterative hanya lebih cepat.
Variasi: Min/Max Query #
Mengganti operator dari + ke min atau max. Identitas juga berubah: identitas min = +∞, identitas max = -∞.
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
class MinSegmentTree {
public:
explicit MinSegmentTree(const std::vector<int>& arr) {
n_ = arr.size();
tree_.assign(4 * n_, INT_MAX);
build(arr, 1, 0, n_ - 1);
}
int range_min(int l, int r) const {
return query_min(1, 0, n_ - 1, l, r);
}
void update(int idx, int val) { update_rec(1, 0, n_ - 1, idx, val); }
private:
int n_;
std::vector<int> tree_;
void build(const std::vector<int>& arr, int node, int lo, int hi) {
if (lo == hi) { tree_[node] = arr[lo]; return; }
int mid = (lo + hi) / 2;
build(arr, 2*node, lo, mid);
build(arr, 2*node + 1, mid+1, hi);
tree_[node] = std::min(tree_[2*node], tree_[2*node + 1]);
}
int query_min(int node, int lo, int hi, int ql, int qr) const {
if (qr < lo || hi < ql) return INT_MAX; // identity for min
if (ql <= lo && hi <= qr) return tree_[node];
int mid = (lo + hi) / 2;
return std::min(query_min(2*node, lo, mid, ql, qr),
query_min(2*node + 1, mid+1, hi, ql, qr));
}
void update_rec(int node, int lo, int hi, int idx, int val) {
if (lo == hi) { tree_[node] = val; return; }
int mid = (lo + hi) / 2;
if (idx <= mid) update_rec(2*node, lo, mid, idx, val);
else update_rec(2*node + 1, mid+1, hi, idx, val);
tree_[node] = std::min(tree_[2*node], tree_[2*node + 1]);
}
};
int main() {
std::vector<int> arr = {5, 2, 8, 1, 9, 3};
MinSegmentTree st(arr);
std::cout << "min [0,5] = " << st.range_min(0, 5) << '\n';
std::cout << "min [0,2] = " << st.range_min(0, 2) << '\n';
std::cout << "min [3,5] = " << st.range_min(3, 5) << '\n';
std::cout << "min [2,4] = " << st.range_min(2, 4) << '\n';
st.update(2, 0); // arr[2] = 0
std::cout << "Setelah update arr[2]=0: min [0,5] = " << st.range_min(0, 5) << '\n';
return 0;
}Output yang Dihasilkan:
min [0,5] = 1
min [0,2] = 2
min [3,5] = 1
min [2,4] = 1
Setelah update arr[2]=0: min [0,5] = 0- Identitas
INT_MAX: bila ada no-overlap, returnINT_MAX(identitas minimum).std::min(x, INT_MAX) = x. - Build/query identik dengan sum version — hanya operator yang berubah:
+→std::min. - Hasil
min [0,5] = 1: nilai terkecil dalam array[5, 2, 8, 1, 9, 3]adalah 1 (di indeks 3). - Setelah update
arr[2] = 0: minimum baru = 0.
Kompleksitas dan Perbandingan #
| Struktur | Build | Range query | Point update | Memori |
|---|---|---|---|---|
| Naif array | O(1) | O(n) | O(1) | O(n) |
| Prefix sum | O(n) | O(1) | O(n) | O(n) |
| Segment tree | O(n) | O(log n) | O(log n) | O(4n) |
| Fenwick tree (BIT) | O(n) | O(log n) | O(log n) | O(n) |
Ringkasan #
- Segment tree: binary tree yang menyimpan aggregate (sum/min/max) dari setiap segmen array dalam node.
- Tiga operasi utama: build O(n), range query O(log n), point update O(log n).
- Implementasi rekursif mudah dibaca; iteratif ~2-3× lebih cepat untuk n besar.
- Operator dapat diganti: sum (+0), min (+∞), max (−∞), GCD (identity = 0), dll.
- Segment tree unggul saat update + query seimbang. Prefix sum unggul saat query-only.
- Trade-off memori:
4n(rekursif) atau2n(iteratif) — lebih besar dari prefix sumn. - Variasi lanjutan: lazy propagation untuk range update (update
[l,r]sekaligus), persistent segment tree untuk akses versi historis. Dibahas di Section 22 (Struktur Data Lanjutan).