# Fenwick Tree
Fenwick Tree 又稱 Binary Indexed Tree(樹狀數組),以下簡稱 BIT,是一個方便解決區間與前綴和問題的資料結構。本篇筆記會介紹其原理,也會說明此資料結構在題目上如何應用
## Fenwick Tree 簡介
### 為什麼需要 Fenwick Tree?
給一個[經典的問題](https://zerojudge.tw/ShowProblem?problemid=e346): 「給定一個長度為 $n$ 的數列 $a_i$ ($1\leq i \leq n$),即 $q$ 比詢問,每筆詢問給定 $l, r$,求區間 $[l, ~r]$ 的總和」這個問題非常基本,定義一個前綴和陣列 $pre_i=\sum_{j=1}^i a_j$,並預處理之,再使用差分 $pre_r - pre_{l - 1}$,即可算出 $a_l + a_{l+1} + ... +a_r$
[Zerojudge e346. 區間和練習](https://zerojudge.tw/ShowProblem?problemid=e346)實作如下:
```cpp
// 以上略
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';
}
```
然而,當題目給的數列「可以在詢問過程中對任一個 $a_i$ 加值 $v$」,那麼前綴和陣列 $pre_i$~$pre_n$ 都加值 $v$。在最差情況下,若都是對 $x_1$ 加值,那麼每次加值時間複雜度為 $O(n)$,而這種複雜度非常糟糕。因此我們需要一個好的資料結構來維護前綴和,使每次修改都可以在很小的時間內完成
### 原理說明
||~~這段不知道也沒關係~~||
#### 查詢前綴和
給定一個長度為 $n$ 的數列 $a$,我們可以建構長度為 $n$ 的陣列 $pre$ ,代表 $pre_i=\sum_{j=1}^i a_j$,其中 $1\leq i\leq n$。考慮將 $pre$ 以二進位的 index 拆分,並以 $s$ 表示。拆分方法是用二進位表示法以 index 最右邊的 $1$ 逐一刪去,並將每次刪去後的結果對應到的 $s_i$ 加總,直到 $i = 0$ 停止。舉例像是 $pre_{11}=s_{11}+s_{10}+s_{8} + s_{0}$
要完成此拆分,需要先建構陣列 $s$,如下表。其中 contents 是指 $a$ 的區間加總
|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|
我們可以定義一個函數 $lowbit(x)$ 代表 $x$ 在二進位表示中最右邊的 $1$ (即 least significant bit)。可以利用補數的特性來定義 $lowbit(x)=x ~\& ~-x$
```cpp
int lowbit(int x) {
return x & -x;
}
```
如此一來,當我們想要詢問第 $k$ 項的前綴和時,就可以使用上述的演算法來完成查詢
```cpp
int query(int x) {
int pre = 0; // 紀錄答案
for (; x; x -= lowbit(x)) // 每次減去 least significant bit
pre += s[x]; // 加上去
return pre;
}
```
#### 更新數值
在更新數值 $a_i$ 時,會發現 $pre_i, pre_{i+1},...,pre_{n}$ 都會受影響。如上述所建構的 $s$,假設我們要使 $a_{11}$ 加值 $v$,我們可以觀察到會受影響的 $s$ 陣列數值有 $s_{11}$、$s_{12}$ 與 $s_{16}$,這剛好會是 $i$ 不斷加上 $lowbit(i)$ 的結果 ($11 + lowbit(11)=12$、$12 + lowbit(12) = 16$)。因此我們可以得到以下算法
```cpp
void update(int i, ll v) { // 將 i 的值加上 v
for (; i < n; i += lowbit(i))
s[i] += v;
}
```
以下面樹狀圖表示會更直覺一點。由於這是以二進制編號來做更新的資料結構,因此提出此想法的原作者將其命名為 Binary Indexed Tree
<img src="https://hackmd.io/_uploads/SJnMXQLt6.png" style="margin: 0 auto; display: block; width: 400px">
<p class="text-center">
圖源: 海大競程 - 2025 離散化+BIT
</p>
#### 建構資料結構
若想要將整個 $a$ 陣列塞入 BIT,可以將陣列 $s$ 先初始化為 $0$,再呼叫 $update()$ 函數去更新數值
**要注意 $s$ 跟 1-based,而 $a$ 是 0-based**
```cpp
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]);
}
```
### 時間複雜度分析
查詢與更新均與樹高有關
- 查詢前綴和 : $O(log ~n)$
- 更新數值 : $O(log ~n)$
- 建構資料結構 : $O(n~log~n)$
## 完整程式碼
變數名稱同上述所示,我們可以用 `struct` 包起來避免變數互相影響
注意有些變數在數字很大的時候可以用 `long long` 宣告
```cpp
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]);
}
};
```
## 經典例題說明
### 區間求和問題
#### 題目
給定一個長度為 $n$ 的數列 $a_i$ ($1\leq i \leq n$),及 $q$ 比詢問,每筆詢問給定 $l, r$,求區間 $[l, ~r]$ 的總和
#### 題解
查詢區間可以使用差分的技巧完成
剩下套模板即可
#### 實作程式碼
```cpp
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';
}
```
### 逆序數對問題
#### 題目
給定一個長度為 $n$ 的數列 $a$。對於 $a$ 而言,若 $a_i > a_j$ 且 $1\leq i < j\leq n$,則稱 $(a_i, a_j)$ 為一組逆序數對。求數列 $a$ 中有多少組逆序數對
舉例假設一長度為 $5$ 的數列 $a = [4, 1, 3, 2, 5]$,則 $a$ 有 $4$ 組逆序數對,分別為: $(4,1), (4,3), (4,2), (3,2)$
#### 限制
$1 \leq n\leq 2\times 10^5$
$-10^9\leq a_i\leq 10^9$, for all $i$
#### 題解
- 觀察一 : 最直觀的方式就是用 $O(n^2)$ 去掃描整個陣列。若開兩個 for 迴圈由右往所掃,可以發現有些數字會重複被掃描到。i.g. 在找到 $(3,2)$ 這組之後又會找到 $(4, 2)$,發現 $2$ 被重新掃過。可以考慮將每個出現的數字出現次數記錄到一個陣列 $cnt[~]$ 之中
- 觀察二 : 若建立了用於計數的 $cnt[~]$,可以發現當我們掃描到新的數字 $a_i$ 時,算出此時的前綴和 $cnt[1]+cnt[2]+...+cnt[a_i - 1]$,就可以知道「比 $a_i$ 還小的數字數量」。由於是由右往左掃描,$a_i$ 位於其他數字左邊,符合 $1\leq i < j\leq n$,又因前綴和只會加到比 $a_i$ 小的數字的數量,所以也符合 $a_i > a_j$。因此每次將「比 $a_i$ 還小的數字數量」加總,就可以得到逆序數對數量
- 觀察三 : 前綴和的資料結構可以用 BIT 來實作,有效減少 $cnt[~]$ 單一數值修改的時間複雜度
#### 實作程式碼
```cpp
// 以上略
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'; // 輸出答案
```
此演算法的時間複雜度為: $O(n~log~n)$
#### 離散化
以上實作之中,我們會以數字的大小來建立資料結構 $bit$。然而,若數值為 $10^9$ 或是負值,則無法正常宣告 $bit$ 中的陣列 $s$,因此我們需要用**離散化**的技巧解決問題
離散化的實作非常簡短,步驟只有以下:
1. 將陣列 $a$ 複製到陣列 $tmp$
2. 排序陣列 $tmp$
3. 找到重複的數並刪除,此時會得到一陣列 $tmp$,可以看作是每個 $tmp_j$ 貼上一個新編號 $j$
4. 將陣列的每個數 $a_i$ 用二分搜尋找到對應的 $tmp_j$,並以 $j$ 作為 $a_i$ 的新值
```cpp
// 這段程式碼是 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();
```
此演算法的時間複雜度為: $O(n~log~n)$
離散化的精華在於,陣列在保持了原本數值大小順序的情況下,縮小數值的值域,是個常見的解題技巧
### 二維偏序問題
#### 題目
給定兩個長度為 $n$ 的陣列 $a, b$,問總共有幾組 $(i, j)$ 滿足 $a[i] \leq a[j]$ 且 $b[i] \leq b[j]$
#### 限制
$1\leq n\leq 2\times 10^5$
$0\leq a_i, b_j\leq 10^9$
#### 題解
可以將每個 $a_i$ 與 $b_i$ 看做數對 $(a_i, b_i)$,合起來就是一個數對陣列 $p = (first, second)$。為了滿足 $a[i] \leq a[j]$,可以先將 數對陣列 $p$ 以 $first$ 做排序,如此一來就可以將問題轉化成對 $second$ 求反的逆序數對
#### 程式碼實作
```cpp
// 以上略
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'; // 輸出答案
```
以上程式碼可以對 $second$ 進行離散化
## 題目練習
[AtCoder Fenwick Tree](https://atcoder.jp/contests/practice2/tasks/practice2_b?lang=en) (裸的)
[CSES Range Update Queries](https://cses.fi/problemset/task/1651) (區間修改?)
[Zerojudge b373. [福州19中]车厢重组](https://zerojudge.tw/ShowProblem?problemid=b373) (逆序數對 or 窮舉)
[ABC231 - F. Jealous Two](https://atcoder.jp/contests/abc231/tasks/abc231_f) (二維偏序)
[Zerojudge a457. TOI2010 第五題:餐廳評鑑](https://zerojudge.tw/ShowProblem?problemid=a457) (離散化 + 逆序數對)
[Zerojudge d542. 10810 - Ultra-QuickSort](https://zerojudge.tw/ShowProblem?problemid=d542) (逆序數對)
[CSES Sliding Window Median](https://cses.fi/problemset/task/1076) (二分搜 + BIT)
[Zerojudge f315. 4. 低地距離](https://zerojudge.tw/ShowProblem?problemid=f315) (計數)
[Codeforces Round 261 D. Pashmak and Parmida's problem](https://mirror.codeforces.com/contest/459/problem/D) (計數)
[Codeforces Beta Round 57 E. Enemy is weak](https://codeforces.com/problemset/problem/61/E) (3-逆序數對)
[LeetCode - 493. Reverse Pairs](https://leetcode.com/problems/reverse-pairs/description/) (2 倍逆序數對)
[Zerojudge d799. 区间求和](https://zerojudge.tw/ShowProblem?problemid=d799) (開兩個 BIT 或當線段樹毒瘤)
----
## 參考資料
[海大競程 - 離散化+BIT](https://hackmd.io/@LeeShoWhaodian/2025-BIT#/)
[A New Data Structure for Cumulative Frequency Tables - peter m. fenwick](https://blogs.asarkar.com/assets/docs/algorithms/Fenwick%20Tree%20-%20Fenwick.pdf)
[CP-Algorithms - Fenwick Tree](https://cp-algorithms.com/data_structures/fenwick.html)
[資訊之芽 - Fenwick tree](https://www.csie.ntu.edu.tw/~sprout/algo2021/homework/hand10.pdf)
[麗山演算法讀書會 - Fenwick(BIT, 樹狀數組)](https://hackmd.io/@Lssh-Algorithm/r1n4upLDO)
[wikipedia - Fenwick tree](https://en.wikipedia.org/wiki/Fenwick_tree)
----
> [ShanC 程式競賽筆記](https://hackmd.io/@ShanC/B1ouGxqcC)
> 作者: ShanC
> 更新: 2025/3/2