樹狀陣列、 Fenwick Tree 或 Binary Indexed Tree ,通常用縮寫 BIT 代表。
是一種 “一種基於二進位制 lowbit ,用於維護(加法、位運算、max、gcd 的)字首和的樹形陣列” 。
可以叫做 一個樹狀陣列 或 一棵 Fenwick Tree 。
重要性質:同時滿足即是陣列又是樹的性質。針對定義域時更多從陣列角度,針對值域時更多從樹角度。
1. lowbit
\(lowbit\) 定義域只在正整數上。
對於某個數正整數 \(x\) ,\(lowbit(x)\) 即 \(x\) 在二進位制下最低位的 \(1\) 到最低位這一段。比如 \(110100\) 的 \(lowbit\) 是 \(100\) 。
1.1 lowbit 樹圖
實線段是樹邊,是實際樹圖的邊。
實箭頭是前向邊,由一棵子樹的根節點連向右邊一棵兄弟子樹的左孫子。
一棵樹最左側的鏈是這棵樹的左鏈。
所有 \(2^{x}\) 的點構成了樹圖的左鏈。
1.2. lowbit 遞增鏈
對給定的值域 \(n\) ,\(x\) 的一條 \(lowbit\) 遞增鏈即 \(x = x + lowbit(x)\ s.t.\ x \leq n\) 上的所有 \(x\) 。
對於 \(y = x + lowbit(x)\) ,\(y\) 在樹圖上是 \(x\) 的父親。
\(x\) 最壞會按照 \(2\) 的冪次次增長,於是遞增鏈長度是 \(O(\log n)\) 級別的。
1.3. lowbit 遞減鏈
\(x\) 的一條 \(lowbit\) 遞減鏈即 \(x = x - lowbit(x)\ s.t.\ x \geq 1\) 上的所有 \(x\) 。
對於 \(y = x - lowbit(x)\) 。
- 若 \(x\) 在樹圖的左鏈上,則 \(y\) 是以 \(0\) 。
- 若 \(x\) 不在樹圖的左鏈上,則 \(y\) 是它左邊一顆兄弟子樹。
以 \(x\) 為根的樹最壞有 \(\log_2 x - 1\) 棵子樹,於是遞減鏈長度是 \(O(\log n)\) 級別的。
lowbit 遞減鏈性質
讓 \(x\) 的遞減鏈訪問到的所有節點為 \(y\) ,則以 \(y\) 為根的所有子樹構成了 \(1 \sim x\) 。
1.4. 樹狀陣列
\(c_{x}\) 即樹上的節點,顯然 \(c\) 陣列構成了一顆樹狀結構。
對於 \(1 \sim n\) 上的陣列 \(a\) ,長度為 \(n\) 的樹狀陣列 \(c\) 用於維護字首和陣列 \(s\) 。
\(c_x\) 可以維護出若干 \(a_y\) 的貢獻總和,即 \(x\) 的 lowbit 遞減鏈上的 \(a_y\) 貢獻總和。
1.4.1 樹狀陣列維護的權值 c_x
詳細的說,在樹圖上以 \(x\) 為根,\(\forall y\) 滿足祖先或自己是 \(x\) 組成的集合為 \(S\) 。則 \(c_x\) 維護的就是集合 \(S\) 。
1.4.2 \(T = \sum_{i = 1}^{x} a_i\) 查詢
根據樹狀陣列權值 \(c_x\) 的意義, \(x\) 的子樹加上所有左兄弟子樹上的節點集合 \(M\) 即是 \(1 \sim x\) 。
在 lowbit 意義下, \(x\) 的 lowbit 遞減鏈訪問到的所有 \(y\) ,\(T = \sum c_y\) 。
1.4.3 \(a_x += k\) 單點加
根據樹狀陣列 \(c_x\) 的意義,若執行 \(a_x += k\) 的單點加,則 \(x\) 的所有祖先 \(y\) 有 \(c_y += k\) 。
在 lowbit 意義下, \(x\) 的 lowbit 遞增鏈訪問到的所有 \(y\) ,只需維護 \(c_y += k\) 。
注意點:\(c_x\) 用以維護若干個 \(a_y\) 的貢獻和,不意味只需要對 \(c\) 進行修改。一次修改需要同時修改 \(a\) 和 \(c\) 。
1.5 樹狀陣列基礎應用:“單點修改,字首查詢”
顯而易見的只需要完成兩個個操作
- 單點加:\(a_x += k\) 。(初始化也是單點加)
- 字首查:\(query\ \sum_{i = 1}^{x} a_i\)
http://oj.daimayuan.top/course/15/problem/634
1.6 樹狀陣列基於高維差分應用:維護加法的“區間加,區間查詢”
1.6.1 適用規則:加法、異或
當維護的字首和是加法字首和時,可以利用加法的差分性質做到區間加,區間查詢。
異或是二進位制下的加法,顯然也有差分性質。但我們並不需要推另一個式子,只需要按照加法的公式建樹,在統計答案時 \(\bmod 2\) 即可。
1.6.2 原理
我們可以快速對 \(a\) 單點加,字首和查詢。
如何快速對 \(a\) 區間加,字首和查詢?維護的元素不能變,依舊需要維護 \(s\) 。但可以對其他元素建樹。
做法是不對 \(a\) 建樹,而對 \(a\) 的差分陣列 \(d\) 建樹。
只需要 \(d_l += k, d_{r + 1} -= k\) 即可完成 \(a_{l \sim r} += k\) 。
接下來要做的只是讓 \(s\) 可以被 \(d\) 表示。
考慮如何維護:
對 \((x + 1) \sum_{i = 1}^{x} d_i\) ,建一個樹狀陣列 \(P\) ,\(P.c_x\) 維護若干個 \(d_y\) 。在查詢 \(s_x\) 時加上字首和乘以 \(x - 1\) 的貢獻。
對 \(\sum_{i = 1}^{x} i \times d_i\) ,建一個樹狀陣列 \(Q\) ,\(Q.c_x\) 維護若干個 \(y \times d_y\) 。在查詢 \(s_x\) 時減去字首和的貢獻。
不妨封裝 \(segmodify, prequery\) 。
區間 \([l, r]\) 查詢即 \(prequery(r) - prequery(l - 1)\) 。
如何初始化:
直接用差分陣列初始化。
如何查詢 \([l, r]\): \(ans = segquery(r) - segquery(l - 1)\)
1.6.3 適用場景
https://www.luogu.com.cn/problem/P1438
https://codeforces.com/contest/1955/problem/E
基於差分維護 \(O(\log n)\) 的區間加和區間查,通常是樹狀陣列一個比較雞肋的功能。它的適用場景通常是用加法建樹,然後轉換成異或形態,再轉換成 01 串翻轉形態。可以斷言,任何 01 串翻轉都可以用兩顆樹狀陣列無腦解決。
同樣的,對定義域“單點修,字首查”的樸素樹狀陣列也是比較雞肋的。樹狀陣列的核心應用在於:對值域建樹->權值樹狀陣列->二維全偏序。
2. 權值樹狀陣列/Fenwick Tree
一種常數和複雜度都非常優秀,快速維護字首的資料結構。
適用場景為二維全偏序: \(i < j, a_i < a_j\) (小於號或大於號均可)。
需要注意: \(k < i < j, a_i < a_j\) 是二維偏序而非二維全偏序。這種情況下,若 \([k, j]\) 視窗是滑動的可以用單調佇列處理。否則只能用線段樹處理。
2.1 應用一:維護單點增刪,查詢第 k 大。
http://oj.daimayuan.top/course/15/problem/636
https://www.luogu.com.cn/problem/P1168
2.2 應用二:二維全偏序,逆序對
http://oj.daimayuan.top/course/15/problem/653
2.3 應用三:二維全偏序,LIS
https://acm.hdu.edu.cn/showproblem.php?pid=5256
2.4 應用四:二維全偏序,掃描線,二維數點
http://oj.daimayuan.top/course/15/problem/686
2.4 應用五:二維全偏序,掃描線,區間不同數之和
http://oj.daimayuan.top/course/15/problem/687
3. 高維樹狀陣列
顧名思義是“利用樹狀陣列維護陣列 \(a\) 的高維字首和 \(s\) ”。
與一維樹狀陣列的程式碼幾乎一模一樣。
一般用處較少,但理解不難。在少數情況下可以避免寫線段樹套線段樹。
http://oj.daimayuan.top/course/15/problem/637
3.1 基於加法差分的二維區間加與區間查詢
https://loj.ac/p/135