Fenwick Tree (Binary Indexed Tree)
Fenwick Tree (hay Binary Indexed Tree — BIT) là cấu trúc dữ liệu cho phép vừa cập nhật một phần tử, vừa tính tổng tiền tố (prefix sum) của mảng, mỗi thao tác chỉ tốn . So với mảng tổng tiền tố tĩnh — truy vấn nhưng mỗi lần cập nhật phải dựng lại trong — và so với cách ngây thơ tính tổng đoạn trong , Fenwick Tree đưa cả hai thao tác về . Bài toán điển hình: cho mảng , xử lý xen kẽ hai loại truy vấn "cộng vào " và "tính ".
So với Segment Tree (xem Segment Tree), Fenwick Tree code ngắn hơn nhiều, hằng số nhỏ và chỉ tốn bộ nhớ; đổi lại nó chỉ xử lý gọn các phép có nghịch đảo (tổng, đếm) chứ không trực tiếp làm min/max.
Ý tưởng / Trực giác
Mấu chốt nằm ở biểu diễn nhị phân của chỉ số. Ta xây một mảng , trong đó không lưu mà lưu tổng của một đoạn con kết thúc tại :
với là bit 1 thấp nhất của . Nói cách khác, phụ trách một đoạn có độ dài đúng bằng .
Vì sao điều này giúp tính prefix sum nhanh? Mọi số nguyên dương đều phân tích được thành tổng các luỹ thừa của 2 (các bit 1 trong biểu diễn nhị phân). Tương ứng, đoạn tách được thành một số ít đoạn con rời nhau mà mỗi đoạn chính là một ô . Ta đi từ , cộng , rồi nhảy về (xoá bit 1 thấp nhất) và lặp lại cho đến khi về 0. Số bit 1 của tối đa là , nên prefix sum chỉ tốn .
Tại sao ? Trong bù 2, là phần bù của cộng 1, nên đảo mọi bit phía trên bit 1 thấp nhất và giữ nguyên bit đó; phép AND giữ lại đúng bit 1 thấp nhất. Ví dụ thì .
Khi cập nhật thêm , ta phải cộng vào mọi ô mà đoạn của nó chứa . Các ô đó lập thành chuỗi (cộng dồn bit thấp nhất), cũng tối đa bước. Hai phép "đi xuống để hỏi" (trừ lowbit) và "đi lên để sửa" (cộng lowbit) đối xứng nhau — đó chính là vẻ đẹp của Fenwick Tree.
Ví dụ chạy tay
Xét mảng (), chỉ số từ 1. Mỗi phụ trách đoạn dài :
i : 1 2 3 4 5 6 7 8
a[i]: 3 2 5 1 4 6 2 7
lowbit: 1 2 1 4 1 2 1 8
phụ trách đoạn:
tree[1] = a[1] = 3 (đoạn [1,1], dài 1)
tree[2] = a[1..2] = 3+2 = 5 (đoạn [1,2], dài 2)
tree[3] = a[3] = 5 (đoạn [3,3], dài 1)
tree[4] = a[1..4] = 3+2+5+1 = 11 (đoạn [1,4], dài 4)
tree[5] = a[5] = 4 (đoạn [5,5], dài 1)
tree[6] = a[5..6] = 4+6 = 10 (đoạn [5,6], dài 2)
tree[7] = a[7] = 2 (đoạn [7,7], dài 1)
tree[8] = a[1..8] = 30 (đoạn [1,8], dài 8)
Có thể hình dung phạm vi phủ của các ô như sau (mỗi = là một phần tử đoạn ô đó phủ):
index: 1 2 3 4 5 6 7 8
tree8: ================================= [1..8]
tree4: ================= [1..4]
tree2: ======= [1..2]
tree6: ======= [5..6]
tree1: === [1..1]
tree3: === [3..3]
tree5: === [5..5]
tree7: === [7..7]
Truy vấn query(6) = tổng . Bắt đầu , cộng dần rồi xoá lowbit:
i = 6 = 110b -> cộng tree[6] = 10, nhảy về 6 - lowbit(6)=6-2 = 4
i = 4 = 100b -> cộng tree[4] = 11, nhảy về 4 - lowbit(4)=4-4 = 0
i = 0 -> dừng
tổng = 10 + 11 = 21
Kiểm chứng: . Đúng. Chỉ cần 2 bước thay vì 6.
Cập nhật update(3, +4) (cộng 4 vào ). Bắt đầu , cộng dần rồi thêm lowbit:
i = 3 = 011b -> tree[3] += 4 (đoạn [3,3] chứa 3), nhảy về 3 + lowbit(3)=3+1 = 4
i = 4 = 100b -> tree[4] += 4 (đoạn [1,4] chứa 3), nhảy về 4 + lowbit(4)=4+4 = 8
i = 8 =1000b -> tree[8] += 4 (đoạn [1,8] chứa 3), nhảy về 8 + 8 = 16 > N
dừng
Các ô (đoạn không chứa chỉ số 3) không bị động đến — đúng như mong đợi.
Cài đặt
Fenwick Tree cơ bản (cập nhật điểm, truy vấn prefix)
struct BIT {
int n;
vector<long long> tree; // dùng long long để tránh tràn số
BIT(int n) : n(n), tree(n + 1, 0) {} // chỉ số chạy từ 1..n
// Cộng delta vào a[i]: leo lên theo i += lowbit(i)
void update(int i, long long delta) {
for (; i <= n; i += i & (-i))
tree[i] += delta;
}
// Tổng tiền tố a[1..i]: tụt xuống theo i -= lowbit(i)
long long query(int i) {
long long s = 0;
for (; i > 0; i -= i & (-i))
s += tree[i];
return s;
}
// Tổng đoạn [l, r] = prefix(r) - prefix(l-1)
long long query(int l, int r) {
return query(r) - query(l - 1);
}
};
Dựng BIT từ mảng có sẵn trong thay vì gọi lần update ():
// Sau khi đã đặt tree[i] = a[i] cho mọi i:
for (int i = 1; i <= n; i++) {
int j = i + (i & (-i));
if (j <= n) tree[j] += tree[i]; // đẩy tổng lên ô cha
}
Bản Python tương đương:
class BIT:
def __init__(self, n):
self.n = n
self.tree = [0] * (n + 1)
def update(self, i, delta):
while i <= self.n:
self.tree[i] += delta
i += i & (-i)
def query(self, i): # tổng a[1..i]
s = 0
while i > 0:
s += self.tree[i]
i -= i & (-i)
return s
def range_query(self, l, r):
return self.query(r) - self.query(l - 1)
Ứng dụng kinh điển: đếm nghịch thế
Nghịch thế là cặp với và . Quét trái sang phải, với mỗi đếm xem có bao nhiêu phần tử đã chèn lớn hơn nó — đó chính là số nghịch thế kết thúc tại . Dùng BIT trên trục giá trị (nén toạ độ nếu giá trị lớn):
// Giả sử a[] đã nén về 1..M (M giá trị phân biệt)
long long countInversions(vector<int>& a, int M) {
BIT bit(M);
long long inv = 0;
for (int x : a) {
// số phần tử đã chèn có giá trị > x = (đã chèn) - (số <= x)
inv += bit.query(M) - bit.query(x);
bit.update(x, 1); // đánh dấu giá trị x xuất hiện
}
return inv;
}
Độ phức tạp
| Thao tác | Thời gian | Lý do |
|---|---|---|
update |
Mỗi bước cộng làm chỉ số tăng ít nhất gấp đôi về "tầng bit", nên tối đa bước tới khi vượt . | |
query |
Mỗi bước xoá một bit 1; có tối đa bit 1. | |
| Dựng cây | kiểu đẩy lên, hoặc nếu gọi lần update. |
|
| Bộ nhớ | Đúng một mảng — không có hệ số 4 như Segment Tree. |
Với truy vấn, tổng thời gian là .
⚠️ Lỗi thường gặp
- Đánh chỉ số từ 0. Fenwick Tree bắt buộc chỉ số bắt đầu từ 1, vì khiến vòng
updatelặp vô hạn (chỉ số không bao giờ tăng) và vòngquerydừng ngay. Nếu dữ liệu gốc từ 0, hãy cộng 1 vào mọi chỉ số. - Tràn số (overflow). Tổng nhiều giá trị lớn dễ vượt
int(ví dụ phần tử cỡ cho tổng ). Luôn dùnglong longcho mảngtreevà biến tích luỹ. Khi đếm nghịch thế, số cặp có thể tới — vượtint. - Quên nén toạ độ (coordinate compression). Khi BIT chạy trên trục giá trị mà giá trị lên tới , không thể cấp mảng kích thước đó. Phải nén về trước (sort + unique + tra cứu). Quên bước này gây tràn bộ nhớ hoặc lỗi truy cập.
- Sai biên với
query(l-1). Tổng đoạn dùngquery(r) - query(l-1). Khi thì vàquery(0)phải trả về 0 (vòngforkhông chạy vì sai ngay) — cài đúng thì an toàn, nhưng nếu vô tình truy cậptree[0]sẽ sai. - Modulo âm. Khi bài yêu cầu in theo modulo và phép tính có trừ (ví dụ
query(r) - query(l-1)rồi% MOD), kết quả có thể âm. Sửa:((x % MOD) + MOD) % MOD. Lưu ý và cấu trúc BIT bản thân không liên quan modulo — chỉ giá trị lưu mới cần. - Cập nhật giá trị tuyệt đối thay vì delta.
update(i, x)cộng chứ không gán . Muốn gán giá trị mới , phải truyềnupdate(i, v - a[i])và nhớ cập nhật lại .
Biến thể / Mở rộng
Cập nhật đoạn + truy vấn đoạn (hai BIT)
Dùng hai BIT kết hợp difference array để vừa cộng vào cả đoạn vừa truy vấn tổng đoạn, tất cả :
struct RangeBIT {
int n;
BIT b1, b2;
RangeBIT(int n) : n(n), b1(n), b2(n) {}
// Cộng v vào mọi phần tử trong [l, r]
void range_update(int l, int r, long long v) {
b1.update(l, v); b1.update(r + 1, -v);
b2.update(l, v * (l - 1)); b2.update(r + 1, -v * r);
}
long long prefix_sum(int i) {
return b1.query(i) * i - b2.query(i);
}
long long range_sum(int l, int r) {
return prefix_sum(r) - prefix_sum(l - 1);
}
};
Fenwick Tree 2 chiều
Cập nhật điểm và truy vấn tổng hình chữ nhật góc – trong :
struct BIT2D {
int n, m;
vector<vector<long long>> tree;
BIT2D(int n, int m) : n(n), m(m), tree(n + 1, vector<long long>(m + 1, 0)) {}
void update(int x, int y, long long delta) {
for (int i = x; i <= n; i += i & (-i))
for (int j = y; j <= m; j += j & (-j))
tree[i][j] += delta;
}
long long query(int x, int y) {
long long s = 0;
for (int i = x; i > 0; i -= i & (-i))
for (int j = y; j > 0; j -= j & (-j))
s += tree[i][j];
return s;
}
// Tổng hình chữ nhật [x1..x2] x [y1..y2] (nguyên lý bù trừ)
long long query(int x1, int y1, int x2, int y2) {
return query(x2, y2) - query(x1 - 1, y2)
- query(x2, y1 - 1) + query(x1 - 1, y1 - 1);
}
};
So sánh với Segment Tree
| Segment Tree | Fenwick Tree | |
|---|---|---|
| Cài đặt | Phức tạp hơn | Đơn giản, ngắn |
| Bộ nhớ | ||
| Hằng số | Lớn hơn | Nhỏ |
| Min/max | Có | Không trực tiếp |
| Cập nhật đoạn | Lazy propagation | Có (hai BIT) |
| 2D | Có (cồng kềnh) | Có (gọn) |
| Khi nào dùng | Cần min/max, lazy phức tạp, đa thao tác | Chỉ cần tổng/đếm, ưu tiên gọn nhẹ |
Bài tập luyện
- Nghịch Thế Cửa Sổ Trượt (swinv) — (Advanced) Đếm nghịch thế trong từng cửa sổ trượt độ dài ; luyện BIT trên trục giá trị (có nén toạ độ) kết hợp thêm/bớt phần tử khi cửa sổ trượt.
- Ảnh cân bằng (bphoto) — (Advanced) Với mỗi phần tử đếm số phần tử lớn hơn ở bên trái và bên phải; áp dụng BIT đếm trực tiếp theo hai lượt quét.
- Cắt tóc (haircut20) — (Advanced) Đếm số nghịch thế khi cắt ngưỡng giá trị tăng dần; rèn cách dùng đóng góp nghịch thế tích luỹ bằng BIT.
- Kệ Thuốc Phép (matchstk) — (Advanced) Quy về số phép đổi chỗ liền kề tối thiểu = số nghịch thế của hoán vị, tính bằng BIT rồi lấy modulo (cẩn thận tràn số và modulo).
- Nhảy Ô — Gold (hopscotchgo) — (Veteran) Kết hợp quy hoạch động với BIT để cộng dồn theo điều kiện thứ tự — bước tiến từ BIT thuần sang BIT làm tăng tốc DP.
- Cắt Cỏ (mowing16) — (Expert) BIT/Segment Tree kết hợp sweep line trên các sự kiện; bài tổng hợp nâng cao để củng cố BIT trong cấu trúc dữ liệu phức tạp.