樹狀陣列快速入門

zsxuan發表於2024-04-10

樹狀陣列、 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)\)

  1. \(x\) 在樹圖的左鏈上,則 \(y\) 是以 \(0\)
  2. \(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 樹狀陣列基礎應用:“單點修改,字首查詢”

顯而易見的只需要完成兩個個操作

  1. 單點加:\(a_x += k\) 。(初始化也是單點加)
  2. 字首查:\(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\) 表示。

\[\begin{aligned} s_x &= \sum_{i = 1}^{x} a_i \\ &= \sum_{i = 1}^{x} \sum_{j = 1}^{i} d_j \\ &= \sum_{i = 1}^{x} \sum_{j = 1}^{x} [j \leq i] d_j \\ &= \sum_{i = 1}^{x} \sum_{j = 1}^{x} [i \leq j] d_i \\ &= \sum_{i = 1}^{x} d_i \sum_{j = 1}^{x} [i \leq j] \\ &= \sum_{i = 1}^{x} d_i (x - i + 1) \\ &= (x + 1) \sum_{i = 1}^{x} d_i - \sum_{i = 1}^{x} i \times d_i \\ \end{aligned} \]

考慮如何維護:

\((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

4. 樹狀陣列快速入門題解報告

相關文章