Cây Phân Đoạn & Cây Fenwick (Segment Tree & BIT)
1. Khái niệm & bản chất của cấu trúc dữ liệu truy vấn đoạn (Range Query Data Structures)
Khi một bài toán có $Q = 10^5$ truy vấn xen kẽ giữa:
- Cập nhật giá trị (Update): Gán $A[i] = X$ hoặc cộng thêm vào $A[i] \mathrel{+}= X$.
- Truy vấn đoạn (Range Query): Tính tổng $\sum_{k=L}^R A[k]$ hoặc tìm $\min_{k=L}^R A[k]$, $\max_{k=L}^R A[k]$, $\gcd_{k=L}^R A[k]$.
Nếu dùng mảng thông thường: Cập nhật $\mathcal{O}(1)$ nhưng truy vấn $\mathcal{O}(N) \implies \mathcal{O}(QN) \approx 10^{10} \implies \text{TLE}$.
Nếu dùng Mảng tiền tố tĩnh: Truy vấn $\mathcal{O}(1)$ nhưng cập nhật lại mảng tiền tố mất $\mathcal{O}(N) \implies \text{TLE}$.
Giải pháp đột phá: Cây Fenwick (Binary Indexed Tree - BIT) và Cây phân đoạn (Segment Tree) cân bằng cả 2 thao tác cập nhật và truy vấn trong thời gian logarit $\mathcal{O}(\log N)$.
2. Cây Fenwick
2.1. Cấu trúc & Thủ thuật bit LSB i & (-i)
Mỗi nút bit[i] quản lý tổng của một đoạn con có độ dài bằng $LSB(i) = i \ \& \ (-i)$ kết thúc tại chỉ số $i$:
- Đoạn quản lý: $(i - LSB(i), i]$.
- Bộ nhớ siêu nhẹ: Đúng $N$ phần tử.
const int MAXN = 1000000;
long long bit[MAXN + 1];
int n;
// Cộng thêm val vào vị trí idx (1-based) trong O(log N)
void update_bit(int idx, long long val) {
for (; idx <= n; idx += idx & (-idx)) {
bit[idx] += val;
}
}
// Tính tổng tiền tố từ 1 đến idx trong O(log N)
long long query_bit(int idx) {
long long sum = 0;
for (; idx > 0; idx -= idx & (-idx)) {
sum += bit[idx];
}
return sum;
}
// Truy vấn tổng đoạn [L, R]
long long range_query(int L, int R) {
return query_bit(R) - query_bit(L - 1);
}
3. Cây phân đoạn
3.1. Cấu trúc cây nhị phân đầy đủ
- Gốc quản lý đoạn toàn cục $[1, N]$. Nút $id$ quản lý $[L, R]$ có hai con: con trái $2 \times id$ quản lý $[L, mid]$ và con phải $2 \times id + 1$ quản lý $[mid + 1, R]$.
- Bộ nhớ mảng: Luôn cấp phát $4N$ phần tử
tree[4 * MAXN]. - Đa năng tuyệt đối: Hỗ trợ mọi hàm có tính kết hợp: Tổng, Min, Max, GCD.
const int MAXN = 200000;
long long tree[4 * MAXN];
long long a[MAXN + 1];
void build_tree(int id, int l, int r) {
if (l == r) {
tree[id] = a[l];
return;
}
int mid = (l + r) / 2;
build_tree(2 * id, l, mid);
build_tree(2 * id + 1, mid + 1, r);
tree[id] = min(tree[2 * id], tree[2 * id + 1]); // Cây Range Minimum Query
}
void update_tree(int id, int l, int r, int pos, long long val) {
if (l == r) {
tree[id] = val;
return;
}
int mid = (l + r) / 2;
if (pos <= mid) update_tree(2 * id, l, mid, pos, val);
else update_tree(2 * id + 1, mid + 1, r, pos, val);
tree[id] = min(tree[2 * id], tree[2 * id + 1]);
}
long long query_tree(int id, int l, int r, int u, int v) {
if (v < l || u > r) return 1e18; // Nằm ngoài khoảng
if (u <= l && r <= v) return tree[id]; // Nằm trọn trong khoảng
int mid = (l + r) / 2;
return min(query_tree(2 * id, l, mid, u, v), query_tree(2 * id + 1, mid + 1, r, u, v));
}
4. Ranh giới áp dụng: Khi nào chọn Fenwick vs Segment Tree?
| Tiêu Chí | Cây Fenwick (BIT) | Cây Phân Đoạn (Segment Tree) |
|---|---|---|
| Độ phức tạp code | Cực ngắn ($\approx 15$ dòng), ít bug | Dài hơn ($\approx 50$ dòng) |
| Tốc độ thực thi | Nhanh hơn gấp 2–3 lần Segment Tree | Chậm hơn do chi phí đệ quy |
| Bộ nhớ | Đúng $N$ phần tử | Cần $4N$ phần tử |
| Phạm vi bài toán | Tổng tiền tố, đếm nghịch thế, tìm $K$-th | Mọi hàm kết hợp (Min, Max, GCD, Lazy Propagation) |