# 樹狀數組
###### tags: `Competitive Programming Note` `108 師大附中校隊培訓`
本文已停止更新,新版請至 [WiwiHo 的競程筆記](https://cp.wiwiho.me/)
[108 師大附中校隊培訓簡報](/@wiwiho/SJ615_FcB)
前綴和序列不能做修改,因為做一次修改就要用 $O(n)$ 的時間來重新算出整個序列,也就是說,每一個位置都被太多前綴和所包含了。
雖然帶修改前綴和也可以用線段樹來做,不過有一個更適合用在前綴和的可修改結構—樹狀數組/二元索引樹(Binary Indexed Tree,常簡稱 BIT)。BIT 的常數小,實作也更簡單。
BIT 的空間只要 $n$,比線段樹的 $4n$ 小,而和前綴和一樣,只需要用一個一維陣列就可以儲存一棵 BIT 了。
BIT 的每一個位置表示的值根據它的位置編號而定,$BIT_i$ 定義為:以 $A_i$ 為結尾,長度為 $lowbit(i)$ 的區間和,也就是 $[i-lowbit(i)+1,i]$ 這個範圍的和,$lowbit(i)$ 指的是把 $i$ 轉成二進位後,把最低位的 $1$ 以外的 $1$ 都變成 $0$,例如 $$lowbit((56)_{10})=lowbit((111000)_2)=(1000)_2=(8)_{10}$$
所以說,$BIT_{56}$ 就是 $[49,56]$ 這個區間的和。
要得到 $lowbit(x)$ 的方法是 `x&-x`,也就是把它和它的相反數取 bitwise and,因為 `-x` 會是 `x` 的補數加 $1$,`x` 在最低位的是 $1$ 的位元會變為 $0$,右邊的 $0$ 在取補數後會變為 $1$,加上 $1$ 之後,這些又會變為 $0$,而本來是最低位的 $1$ 的地方也會變為 $1$,但在左邊的每一個位元都會和本來不同,因為取 bitwise and 後,就可以得到 $lowbit(x)$。
節點 $0$ 代表的是一個空區間,而節點 $1$ 的區間只包含 $[1,1]$,因此,序列的索引應該要從 $1$ 開始。
那麼 BIT 畫成樹是什麼樣子?其實它並不是二元樹,binary 指的是二進位。

- 節點 $i$ 的父節點是 $i-lowbit(i)$。
- 節點 $i$ 的深度與 $i$ 是 $1$ 的位元數相同。
- 節點 $i$ 的右兄弟節點(如果有的話)是 $i+lowbit(i)$。
節點 $0$ 就是根節點,然後按照上述規則,可以畫出一棵樹。仔細觀察一下,會發現每個節點代表的區間有這些性質:
- 節點 $i$ 的父節點是 $p$,則節點 $i$ 的區間是 $[p+1,i]$,因為 $p=i-lowbit(i)$。
- 節點 $i$ 的區間包含它所有左兄弟節點的區間,因為它們的父節點都一樣,所以它們區間的起點都一樣,但 $i$ 的結束點較後面。
- 節點 $i$ 的區間包含所有左兄弟節點 $j$ 的子孫節點的區間,因為 $j$ 子孫節點區間必在 $j$ 和 $i-1$ 之間,而 $i$ 的區間肯定包含這段。
在知道這些性質後,就可以來討論怎麼處理詢問了。
## 單點修改
如果現在要修改一個位置 $pos$,那要找到所有區間包含這個位置的節點,第一個這樣的節點是 $pos$,接下來,它的所有右兄弟節點都包含這裡,用 $pos+lowbit(pos)$ 可以得到右兄弟節點,而其父節點的右兄弟節點也包含這個點,若 $pos$ 最右邊的兄弟節點是 $t$,那其父節點的右兄弟會是 $t+lowbit(t)$。
所以只要一直把 $pos$ 加上 $lowbit(pos)$,就可以得到它和它祖先的所有右兄弟節點,修改這些節點的答案就好了。
實作相當簡單:
```
void modify(int pos, int x){ //把pos改成x
//n是序列大小
for(; pos <= n; pos += lowbit(pos)){
修改節點pos的答案;
}
}
```
可以發現 $lowbit(pos)$ 會不斷增加,所以複雜度是 $O(\log n)$。
## 前綴查詢
要查詢以 $pos$ 為結尾的前綴,就應該找到幾段不重疊,且聯集恰好是所求前綴的區間。
區間以 $pos$ 為結尾的節點是 $pos$,而節點 $pos$ 的區間剛好緊接在它的父節點之後,它的父節點是 $pos - lowbit(pos)$,所以只要找到 $pos$ 和它所有祖先節點,這些區間聯集起來就是我們想要的前綴。
實作也非常簡單:
```
type query(int pos){
type ans;
for(; pos > 0; pos -= lowbit(pos)){
更新ans;
}
return ans;
}
```
同樣地,$lowbit(pos)$ 會不斷增加,因此複雜度是 $O(\log n)$。
## 建構
建 BIT 的話,就把每一個元素分別 modify 就好了,複雜度是 $O(n \log n)$。
## 高維 BIT
和線段樹一樣,在 $D$ 維 BIT 的每個節點上存一棵 $D-1$ 維的 BIT,用法就和線段樹完全一樣。
## 應用
BIT 最基礎的應用就是拿來算前綴和,因為和是一種「可返回」的東西,減去本來的值在加上新的值就可以得到新的和,而有了前綴和就也可以算區間和了,且它比線段樹好寫很多。
BIT 前綴和和差分序列結合使用,就可以做到 $O(1)$ 區間修改、$O(\log n)$ 單點查詢。
BIT 也可以區間修改、區間查詢前綴和。先算出要做的序列 $A$ 的差分序列 $D$,$A$ 的索引從 $1$ 開始,然後我們可以得出 $[1,x]$ 這個區間的和是:
$$\sum_{i=1}^x a_i = \sum_{i=1}^x D_i \times (x-i+1)\\
=D_1 \times x + D_2 \times (x-1) + ... + D_x \times 1$$
然後可以把它化成:
$$(x+1) (\sum_{i=1}^x D_i) - (\sum_{i=1}^x D_i \times i)$$
那麼只要用兩個 BIT,一個維護 $D_i$、另一個維護 $D_i \times i$,這樣就可以做區間修改、區間求和了。
至於 BIT 可不可以做最大最小值這種「不可返回」的操作呢?如果在求最大值的數字時,把其中一個位置改小,那麼是沒辦法單靠被修改的位置就得出新的最大值的,因此如果要用 BIT 做最大值,數字只能被改大,做最小值,數字只能被改小。
## 練習題
- [ZeroJudge d799](https://zerojudge.tw/ShowProblem?problemid=d799) - 區間加值、詢問區間和
- [TIOJ 1175](https://tioj.ck.tp.edu.tw/problems/1175) - LIS
- [TIOJ 1869](https://tioj.ck.tp.edu.tw/problems/1869) - 二維 BIT
- [TIOJ 1080](https://tioj.ck.tp.edu.tw/problems/1080) - 逆序數對