Fenwick Tree 又稱 Binary Indexed Tree(樹狀數組),以下簡稱 BIT,是一個方便解決區間與前綴和問題的資料結構。本篇筆記會介紹其原理,也會說明此資料結構在題目上如何應用
給一個經典的問題: 「給定一個長度為
// 以上略
int n, q, l, r;
cin >> n;
vector<long long> a(n), pre(n + 1, 0);
for (int i = 0; i < n; i++) {
cin >> a[i];
pre[i + 1] = pre[i] + a[i];
}
cin >> q;
while (q--) {
cin >> l >> r;
cout << pre[r] - pre[l - 1] << '\n';
}
然而,當題目給的數列「可以在詢問過程中對任一個
這段不知道也沒關係
給定一個長度為
要完成此拆分,需要先建構陣列
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|
contents | 0 | 1 | 1~2 | 3 | 1~4 | 5 | 5~6 | 7 | 1~8 |
index | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
---|---|---|---|---|---|---|---|---|
contents | 9 | 9~10 | 11 | 9~12 | 13 | 13~14 | 15 | 1~16 |
我們可以定義一個函數
int lowbit(int x) {
return x & -x;
}
如此一來,當我們想要詢問第
int query(int x) {
int pre = 0; // 紀錄答案
for (; x; x -= lowbit(x)) // 每次減去 least significant bit
pre += s[x]; // 加上去
return pre;
}
在更新數值
void update(int i, ll v) { // 將 i 的值加上 v
for (; i < n; i += lowbit(i))
s[i] += v;
}
以下面樹狀圖表示會更直覺一點。由於這是以二進制編號來做更新的資料結構,因此提出此想法的原作者將其命名為 Binary Indexed Tree
圖源: 海大競程 - 2025 離散化+BIT
若想要將整個
要注意
vector<int> s;
void construct(vector<int> a) {
n = a.size();
s.clear(), s.resize(n + 1, 0);
for (int i = 1; i <= n; i++)
update(i, a[i - 1]);
}
查詢與更新均與樹高有關
變數名稱同上述所示,我們可以用 struct
包起來避免變數互相影響
注意有些變數在數字很大的時候可以用 long long
宣告
struct Fenwick {
int n;
vector<int> s;
int lowbit(int x) { return x & -x; }
Fenwick(int _n) {
n = _n + 1;
s.clear(), s.resize(n, 0);
}
void update(int i, int v) { // 更新數值
for (; i < n; i += lowbit(i))
s[i] += v;
}
int query(int x) { // 查詢前綴和
int pre = 0;
for (; x; x -= lowbit(x))
pre += s[x];
return pre;
}
Fenwick(vector<int> a) { // 建構資料結構
n = a.size(); // a 是 0-based
s.clear(), s.resize(n + 1, 0); // s 是 1-based
for (int i = 1; i <= n; i++)
update(i, a[i - 1]);
}
};
給定一個長度為
查詢區間可以使用差分的技巧完成
剩下套模板即可
int l, r, n, q;
long long a;
cin >> n; // 輸入陣列長度
Fenwick bit(n);
for (int i = 0; i < n; i++)
cin >> a, bit.update(i + 1, a); // 輸入陣列, 更新 bit
cin >> q; // 輸入詢問數
while (q--) {
cin >> l >> r; // 輸入區間
cout << bit.query(r) - bit.query(l - 1) << '\n';
}
給定一個長度為
舉例假設一長度為
觀察一 : 最直觀的方式就是用
觀察二 : 若建立了用於計數的
觀察三 : 前綴和的資料結構可以用 BIT 來實作,有效減少
// 以上略
int n, ans = 0;
cin >> n; // 輸入長度
vector<int> a(n); // 0-based
for (int i = 0; i < n; i++)
cin >> a[i]; // 輸入陣列
Fenwick bit(mxn); // 開數字最大的 BIT
for (int i = n - 1; i >= 0; i--) {
ans += bit.query(a[i] - 1); // 找小於 a[i] 的數量
bit.update(a[i], 1); // 更新數量
}
cout << ans << '\n'; // 輸出答案
此演算法的時間複雜度為:
以上實作之中,我們會以數字的大小來建立資料結構
離散化的實作非常簡短,步驟只有以下:
// 這段程式碼是 0-based
vector<int> tmp(a); // 將 a 複製到 tmp
sort(tmp.begin(), tmp.end()); // 將其排序
tmp.erase(unique(tmp.begin(), tmp.end()), tmp.end()); // 找到並刪除重複的數
for (int i = 0; i < n; i++) // 重新編號
a[i] = lower_bound(tmp.begin(), tmp.end(), a[i]) - tmp.begin();
此演算法的時間複雜度為:
離散化的精華在於,陣列在保持了原本數值大小順序的情況下,縮小數值的值域,是個常見的解題技巧
給定兩個長度為
可以將每個
// 以上略
typedef pair<int, int> pii;
int n;
cin >> n; // 輸入長度
vector<pii> p(n);
for (int i = 0; i < n; i++) {
cin >> p[i].first >> p[i].second;
p[i].first++, p[i].second++; // 因為 BIT 是 1-based
}
sort(p.begin(), p.end(), [&](pii a, pii b) {
if (a.first == b.first)
return a.second < b.second;
return a.first < b.first;
}); // 對 first 排序
Fenwick bit(n); // 宣告 bit
int ans = 0; // 紀錄答案
for (int i = 0; i < n; i++) { // 求逆序數對
ans += bit.query(p[i].second - 1);
bit.update(p[i].second, 1);
}
cout << ans << '\n'; // 輸出答案
以上程式碼可以對
AtCoder Fenwick Tree (裸的)
CSES Range Update Queries (區間修改?)
Zerojudge b373. [福州19中]车厢重组 (逆序數對 or 窮舉)
ABC231 - F. Jealous Two (二維偏序)
Zerojudge a457. TOI2010 第五題:餐廳評鑑 (離散化 + 逆序數對)
Zerojudge d542. 10810 - Ultra-QuickSort (逆序數對)
CSES Sliding Window Median (二分搜 + BIT)
Zerojudge f315. 4. 低地距離 (計數)
Codeforces Round 261 D. Pashmak and Parmida's problem (計數)
Codeforces Beta Round 57 E. Enemy is weak (3-逆序數對)
LeetCode - 493. Reverse Pairs (2 倍逆序數對)
Zerojudge d799. 区间求和 (開兩個 BIT 或當線段樹毒瘤)
海大競程 - 離散化+BIT
A New Data Structure for Cumulative Frequency Tables - peter m. fenwick
CP-Algorithms - Fenwick Tree
資訊之芽 - Fenwick tree
麗山演算法讀書會 - Fenwick(BIT, 樹狀數組)
wikipedia - Fenwick tree
ShanC 程式競賽筆記
作者: ShanC
更新: 2025/3/2