本文旨在講解:node
樹狀數組或二元索引樹(英語:Binary Indexed Tree),又以其發明者命名爲 \(\mathrm{Fenwick}\) 樹。最先由 \(\mathrm{Peter\; M. Fenwick}\) 於1994年以 《A New Data Structure for Cumulative Frequency Tables[1]》爲題發表在 《SOFTWARE PRACTICE AND EXPERIENCE》。其初衷是解決數據壓縮裏的累積頻率(Cumulative Frequency)的計算問題,現多用於高效計算數列的前綴和, 區間和。它能夠以 \(\mathcal{O(\log n)}\) 的時間獲得任意前綴和(區間和)。ios
不少初學者確定和我同樣,只知曉 BIT 代碼精煉,語法簡明。對於原理好像瞭解,卻又如霧裏探花總感受隔着些什麼。c++
按照 Peter M. Fenwick 的說法,BIT 的產生源自整數與二進制的類比。算法
Each integer can be represented as sum of powers of two. In the same way, cumulative frequency can be represented as sum of sets of subfrequencies. In our case, each set contains some successive number of non-overlapping frequencies.數組
簡單翻一下:每一個整數能夠用二進制來進行表示,在某些狀況下,序列累和(這裏沒有翻譯爲頻率)也能夠用一組子序列累和來表示。在本例子中,每一個集合都有一些連續不重疊的子序列構成。數據結構
實際上, BIT 也是採用相似的想法,將序列累和類比爲整數的二進制拆分,每一個前綴和拆分爲多個不重疊序列和,再利用二進制的方法進行表示。這與 Integer 的位運算很是類似。app
之因此命名爲: Binary Indexed Tree,在論文中 Fenwick 有以下解釋:學習
In recognition of the close relationship between the tree traversal algorithms and the binary representation of an element index,the name "binar indexed tree" is proposed for the new structure.ui
也就是考慮到:樹的遍歷方法與二值表示之間的緊密聯繫,所以將其命名爲二元索引樹。spa
在介紹原理以前先對於一些關鍵的符號作出定義:
在學習 BIT 時,很容易忽略 BIT 設計的思想,而僅僅停留在對於其代碼簡潔精煉的讚歎上,因此第一步咱們將體會 BIT 是如何類比;如何設計;如何實現的。
如上圖所示:咱們給定一個整數: \(num = 13\)
咱們嘗試將 \(num\) 用二進制進行表示: \(1101_2 = 1000_2 + 100_2 + 1_2\) 。能夠看到 \(num\) 能夠由\(3\)個二進制數組成。且拆分的個數老是 \(\mathcal{O(\log_2n)}\) 級的,所以我猜測Fenwick便開始思考如何將一個子序列,藉助二進制的特色快速的表示出來。
首先,依據最簡單的拆分方法(即與二進制拆分相同)如圖左示。顯然這個方法具備缺陷,某些序列會被重複計算,而有些序列則沒有被包含在內,所以解決問題的關鍵,同時也是 BIT 的核心思想即是如何基於編號,構件一個不重疊的子序列集合。
如右圖所示,該拆分方案能很好的實現不重疊的子序列集合,咱們嘗試將其列出以發現其中的規律:
通過觀察:
設某編號的二進制爲 \(\mathrm{XXX}bit\mathrm{XXX}_2\) ,設 \(bit\) 爲當前須要考慮的位\((bit=1)\),\(\mathrm{X}\) 爲\(0 \;or\; 1\) ,則其表示的範圍是:
\([XXX0000_2 + 1, XXX0000_2 + bit000_2]\) ,換一句話說:假如序列編號在 \(bit\) 位爲1,則其表明的子序列具備以下性質:
假如咱們逆序的看待以前\(num=13=1101_2\)的例子:
首先處理\(bit=1\)這一位,其表明的範圍是:\([1100_2 + 0001_2, 1100_2 + 0001_2]\)。而後在\(num\)上減去他:\(num -= (1 << (bit-1)) = 1100_2\)
而後,咱們處理\(bit=3\)這一位:其表明的範圍是:\([1000_2 + 0001_2, 1000_2 + 0100_2]\)。一樣,咱們在\(num\)上減去它。
最後咱們處理\(bit=4\)這一位:其表明的範圍是:\([0000_2 + 0001_2, 0000_2 + 1000_2]\)。至此,處理結束。
咱們回顧整個處理流程,能夠驚訝的發現,若是咱們按照逆序處理,咱們每次處理的\(bit\)都是當前編號的最後的爲1位。咱們將每次處理的\(bit\)定義爲 \(\mathrm{lowbit}\) (note:這是 BIT 中重要的概念)
用通俗的語言:每一個 \(\mathrm{lowbit}\) 都表明其管轄的某一段子序列,又由於 \(\mathrm{lowbit}\) 的值會隨着處理不斷增大,其控制的範圍也會不斷增大。其控制範圍爲:\([cur - lowbit(cur) + 1, cur]\)
如:\(c[13] = tree[13] + tree[12] + tree[8]\)
所以,咱們能夠作出以下總結:
BIT 的原理類比自 Integer 的二進制表示。
BIT 對應的數組 \(tree[i] := 子序列 i 的值\) ,每一個 \(tree[i]\) 控制 \([i - \mathrm{lowbit(i)}+1, i]\) 範圍內的\(f[i]\)值。
利用BIT計算 \(c[i]\) 時,經過相似整數的二進制拆分,將 \(c[i]\) 拆分爲 \(\mathcal{O(\log_2 n)}\) 個 \(tree[j]\) 進行求解。求解的流程爲不斷累加 \(tree[i]\) 並置 $ i \leftarrow i - \mathrm{lowbit(i)}$
計算流程的僞代碼: let ans <- 0 while i > 0: sub_sum <- tree[i] // 獲取子序列累和 i <- i - lowbit(i) // 更新 i ans <- ans + sub_sum return ans
上圖是樹狀數組很是經典的展現圖,經過此圖能夠快速的瞭解:\(tree[i] := \sum \limits_{i - \mathrm{lowbit}(i)+1}^{i}f[i]\) 對應的含義。
到這裏仍是不由感嘆一句:「文章本天成,妙手偶得之」,BIT 這個數據結構實在是精巧。
定義 bitcnt(x) := x二進制中 1 的個數
,則根據前文的分析,計算 \(c[i]\) 時類比整數的二進制拆分,咱們只須要計算 \(bitcnt(i)\) 個子序列的和。每一個子序列經過不斷進行 \(\mathrm{lowbit}\) 運算進行獲取。
\(\mathrm{lowbit}\) 運算爲取數 \(x\) 的最低位的 1 ,最經常使用的方法爲:\(\mathrm{lowbit(x)= (x \& (-x))}\)
上圖展現了一個大小爲 \(16\) 的 BIT,能夠經過圖示清楚的理解 BIT query 的原理:即不斷詢問當前 \(i\) 指示的子序列和(\(tree[i]\)),並經過 \(\mathrm{lowbit}\) 運算指向下一個子序列和。
其 C++
代碼以下:
T tree[maxn]; template <typename T> T query(int i){ T res = 0; while (i > 0){ res += tree[i]; i -= lowbit(i); } return res; }
update 實際上能夠當作 query 的逆過程,簡單來講便是:若要將 \(f[i] += x\),則從 \(tree[i]\) 開始不斷向上更新直到達到 BIT 的上界。
上圖展現了 BIT 更新的流程,這裏主要說明其中一個須要注意的點:爲何咱們首先須要更新 \(tree[i]\) 而不是其餘的,如何保證這就是起始點?(能夠本身思考一下)
這是我曾在學習 BIT 的過程當中比較困惑的一個點:答案在於 \(tree[i]\) 所管轄的子序列範圍,咱們知道 \(tree[i] 管轄 [i - lowbit(i) + 1, i]\) 這個範圍,所以 \(tree[i]\) 是第一個管轄 \(f[i]\) 的元素,因此咱們只須要從這個位置不斷向上更新便可。
其 C++
代碼以下:
int n; // BIT 的大小, BIT index 從 1 開始 T tree[maxn]; template <typename T> void add(int i, T x){ while (i <= n){ tree[i] += x; i += lowbit(i); } }
template<typename T> struct BIT{ #ifndef lowbit #define lowbit(x) (x & (-x)); #endif static const int maxn = 1e3+50; int n; T t[maxn]; BIT<T> () {} BIT<T> (int _n): n(_n) { memset(t, 0, sizeof(t)); } BIT<T> (int _n, T *a): n(_n) { memset(t, 0, sizeof(t)); /* 從 1 開始 */ for (int i = 1; i <= n; ++ i){ t[i] += a[i]; int j = i + lowbit(i); if (j <= n) t[j] += t[i]; } } void add(int i, T x){ while (i <= n){ t[i] += x; i += lowbit(i); } } /* 1-index */ T sum(int i){ T ans = 0; while (i > 0){ ans += t[i]; i -= lowbit(i); } return ans; } /* 1-index [l, r] */ T sum(int i, int j){ return sum(j) - sum(i - 1); } /* href: https://mingshan.fun/2019/11/29/binary-indexed-tree/ note: C[i] --> [i - lowbit(i) + 1, i] father of i --> i + lowbit(i) node number of i --> lowbit(i) */ };
樹狀數組(BIT)的主要優點在於:
query
與 update
操做時間複雜度都只須要 \(\mathcal{O(\log n)}\) 。lazy tag
也存在影響)。而缺點在於:
樹狀數組通常用於解決大部分基於區間上的更新以及求和問題。
下面來談一談線段樹和樹狀數組在使用上的不一樣:
線段樹與樹狀數組的區別 線段樹和樹狀數組的基本功能都是在某一知足結合律的操做(好比加法,乘法,最大值,最小值)下,\(\mathcal{O}(\log n)\)的時間複雜度內修改單個元素而且維護區間信息。
不一樣的是,樹狀數組只能維護前綴「操做和」(前綴和,前綴積,前綴最大最小),而線段樹能夠維護區間操做和。可是某些操做是存在逆元的(即:能夠用一個操做抵消部分影響,減之於加,除之於乘),這樣就給人一種樹狀數組能夠維護區間信息的錯覺:維護區間和,模質數意義下的區間乘積,區間 \(\mathrm{xor}\) 和。能這樣作的本質是取右端點的前綴結果,而後對左端點左邊的前綴結果的逆元作一次操做,因此樹狀數組的區間詢問實際上是在兩次前綴和詢問。
因此咱們能看到樹狀數組能維護一些操做的區間信息但維護不了另外一些的:最大/最小值,模非質數意義下的乘法,緣由在於這些操做不存在逆元,因此就無法用兩個前綴和作。
總結來講:線段樹只須要保證區間操做的可結合性,可加性(即一個大區間的結果能夠由較小區間的結果計算獲得);而樹狀數組除了須要知足上述條件,還須要知足可抵消性,也就是能夠經過一個操做抵消掉不須要區間的貢獻(由於 BIT 只能維護前綴結果)。僅爲我的看法
很是簡單,只須要套模板便可。
// 上述模板部分省略 using ll = long long; const int maxn = 1e6+50; ll f[maxn]; int main(){ ios::sync_with_stdio(0); cin.tie(0); int n; cin >> n; int q; cin >> q; for (int i = 1; i <= n; ++ i) cin >> f[i]; BIT<ll> bit(f, n); for (int i = 0; i < q; ++ i){ int type; cin >> type; if (type == 1){ int i, x; cin >> i >> x; bit.add(i, (ll) x); }else { int l, r; cin >> l >> r; cout << bit.sum(l, r) << '\n'; } } return 0; }
該模板題則難上許多,須要對問題分析建模。
咱們須要考慮如何建模表示 \(tree\) 數組。
首先,設更新操做爲:在 \([l, r]\) 上增長 \(x\)。咱們考慮如何建模維護新的區間前綴和 \(c^{\prime}[i]\)。
下面分狀況討論:
這種狀況下,不須要任何處理, \(c^{\prime}[i] = c[i]\)
這種狀況下,\(c^{\prime}[i] = c[i] + (i - l + 1) \cdot x\)
這種狀況下,\(c^{\prime}[i]=c[i] + (r-l+1)\cdot x\)
所以以下圖所示,咱們能夠設兩個 BIT,那麼\(c^{\prime}[i] = \mathrm{sum(bit_1,i)+sum(bit_2,i) \cdot i}\),對於區間修改等價於:
#include <bits/stdc++.h> using namespace std; // 模板代碼省略 // 這裏作的是單點查詢,可是實現的爲區間查詢 using ll = long long; ll get_sum(BIT<ll> &a, BIT<ll> &b, int l, int r){ auto sum1 = a.sum(r) * r + b.sum(r); auto sum2 = a.sum(l - 1) * (l - 1) + b.sum(l - 1); return sum1 - sum2; } int n, q; const int maxn = 1e6 + 50; ll f[maxn]; int main(){ // ios::sync_with_stdio(0); // cin.tie(0); cin >> n >> q; BIT<ll> bit1, bit2; for (int i = 1; i <= n; ++ i) cin >> f[i]; bit1.init(n), bit2.init(f, n); for (int i = 0; i < q; ++ i){ int type; cin >> type; if (type == 1){ int l, r, x; cin >> l >> r >> x; bit2.add(l, (ll) -1 * (l - 1) * x), bit2.add(r + 1, (ll) r * x); bit1.add(l, (ll) x), bit1.add(r + 1, (ll) -1 * x); }else { int i; cin >> i; cout << get_sum(bit1, bit2, i, i) << '\n'; } } return 0; }
BIT 求解逆序對是很是方便的,在初學時我沒有想到過 BIT 還能用於求解逆序對。在這裏我借逆序對來引出一個小技巧:離散化
BIT 求逆序對的方法很是簡單,逆序對指:i < j and a[i] > a[j]
,統計逆序對實際上就是統計在該元素 a[i]
以前有多少元素大於他。
咱們能夠初始化一個大小爲 \(maxn\) 的空 BIT(全爲0)。隨後:
a[i]
,計算區間 [1, a[i]]
的和,更新答案 ans = i - sum([1, a[i]])
a[i]
的值,tree[a[i]] <- tree[a[i]] + 1
舉個例子:
eg: [2,1,3,4] BIT: 0, 0, 0, 0 >2, sum(2) = 0, ans += 0 - sum(2) -> ans = 0 BIT: 0, 1, 0, 0 >1, sum(1) = 0, ans += 1 - sum(1) -> ans = 1 BIT: 1, 1, 0, 0 >3, sum(3) = 2, ans += 2 - sum(3) -> ans = 1 BIT: 1, 1, 1, 0 >4, sum(4) = 3, ans += 3 - sum(4) -> ans = 1
實際上,即是藉助 BIT 高效計算前綴和的性質實現了快速打標記,先統計在我以前有多少個標記(這些都是合法對),再將本身所在位置的標記加 \(1\)。
所以,很容易寫出這段代碼:
// 僅保留核心代碼 int reversePairs(vector<int>& nums) { int n = nums.size(); if (n == 0) return 0; int mx = *max_element(nums.begin(), nums.end()); BIT<int> bit(mx); // 由於最大隻到最大值的位置 int ans(0); for (int i = 0; i < n; ++ i){ ans += (i - bit.sum(nums[i])); bit.add(nums[i], 1); } return ans; }
可是這個代碼有很是嚴重的問題,首先假如 mx = 1e9
就會出現段錯誤;或者假如 nums[i] < 0
則會出現訪問越界的問題,可是實際上題目中說明了:數組最多隻有 50000個元素,也就是咱們須要想辦法將座標離散化,保留其大小順序便可。
#define lb lower_bound #define all(x) x.begin(), x.end() const int maxn = 5e4 + 50; struct node{ int v, id; }f[maxn]; // 離散化結構體 int arr[maxn]; bool cmp(const node&a, const node &b){ return a.v < b.v; } class Solution { public: int reversePairs(vector<int>& nums) { int n = nums.size(); if (n == 0) return 0; BIT<int> bit(n); for (int i = 1; i <= n; ++ i){ f[i].v = nums[i - 1], f[i].id = i; // 賦值用於排序 } sort(f + 1, f + 1 + n, cmp); int cnt = 1, i = 1; while (i <= n){ /* 用於去重,當有相同元素時其對應的 cnt 應該相同 */ if (f[i].v == f[i - 1].v || i == 1) arr[f[i].id] = cnt; else arr[f[i].id] = ++cnt; ++ i; } int ans = 0; for (int i = 0; i < n; ++ i){ int pos = arr[i + 1]; ans += i - bit.sum(pos); bit.add(pos, 1); } return ans; } };
上面的方法是離散化操做的一種方式,有一點複雜,須要注意的細節比較多。
實際上,該方法即是經過保留每一個元素的所在位置,並將其排序,排序後本身在第 \(i\) 個則將其值 arr[id] = i
離散化爲 \(i\) 。這樣既能夠避免負數,過大的數形成的訪問或者內存錯誤,也充分的保留了各元素之間的大小關係。
離散化的複雜度爲 \(\mathcal{O(\log n)}\) ,實際上也就是排序的複雜度。
總結:離散化--結構體方法
通用性:★★
- 設置結構體
node
,包含屬性val
與id
,初始化結構體數組f
和離散化數組arr
。- 排序
f
,並從1
開始遍歷,arr[f[i].id] = i
,將val
值更新爲k-th min
也就是其在元素中按大小排列的編號。
能夠發現,結構體方法對於空間要求較大,且在去重方面須要下功夫,稍後咱們會講解另外一種離散化方法,你也能夠試試用後文的離散化方法再次解決這題。
能夠看到這題與逆序對的區別在於,翻轉對的定義是:i < j
且 a[i] > 2*a[j]
。其大小關係發生了變化,再也不是原來單純的大小關係,而存在值的變化。
咱們能夠思考下可否用結構體進行離散化,簡單思考後發現:假如第 i
個元素離散化以後的編號爲 id1
,則咱們沒法肯定編號爲 2 * id1
所對應元素的 val
值之間的關係。可能出現以下狀況:
id1 = 1, val = 2 2 * id1 = 2, val' = 3
因此,咱們須要思考一個新的方法來進行離散化。須要注意的是,咱們的關鍵點在於:如何快速的詢問一個元素在一個數組中是第幾大的元素。好比,在數組中快速詢問某個值的兩倍是第幾大的。
實際上,稍微有基礎的話答案便很是清晰:二分查找,咱們能夠首先將數組進行排序,利用 \(lower_{bound}\) 快速找到第一個大於等於該元素所對應的位置,用代碼來講的話:pos = lower_bound(nums.begin(), nums.end(), x) - nums.begin() + 1
。
eg: nums = [3, 2, 4, 7] farr = sort(nums) -> farr = [2, 3, 4, 7] pos(4) = lower_bound(..., 4) - farr.begin() + 1 = 3 即可以快速找到 4 的編號爲 3 (1-index)
可是,有一個問題須要注意:
eg: nums = [3, 2, 5, 7] farr = sort(nums) -> farr = [2, 3, 5, 7] pos(4) = lower_bound(...,4) - farr.beign() + 1 = 3 但實際上,5 > 4,此次詢問錯誤了!!!
爲何會出現詢問錯誤的狀況呢?(所以咱們須要找到的是最後一個小於等於元素 x
的對應位置,而二分查找是大於等於 x
的第一個元素,當原數組中不存在 x
時,便會出現詢問出錯的狀況。)
有多種方法能夠解決這個問題,可是最爲方便的仍是直接將須要查詢的元素所有加進去,也就是 2 * x
所有添加到數組中,從而保證必定存在該元素,又由於 lower_bound
的性質,咱們無需去重。
using vi = vector<int>; using vl = vector<ll>; #define complete_unique(x) (x.erase(unique(x.begin(), x.end()), x.end())) #define lb lower_bound class Solution { public: int reversePairs(vector<int>& nums) { vl tarr; for (auto &e: nums){ tarr.push_back(e); tarr.push_back(2ll * e); // 直接把須要離散化的對應元素加入 } sort(tarr.begin(), tarr.end()); int n = nums.size(); BIT<int> bit(2 * n); // 注意,由於加入了兩倍的元素,因此對應也要開大一點 int res = 0; for (int i = 0; i < n; ++ i){ res += i - bit.sum(lb(tarr.begin(), tarr.end(), 2ll * nums[i]) - tarr.begin() + 1); bit.add(lb(tarr.begin(), tarr.end(), nums[i]) - tarr.begin() + 1, 1); } return res; } };
總結:離散化--二分查找方法
通用性:★★★★★
- 初始化數組
farr
,將元素以及須要尋找的元素都加入其中- 二分查找便可。
二維 BIT 實際上就是套娃,一層層套便可。
其複雜度爲 \(\mathcal{O(\log n \times \log m)}\) ,\(n,m\)分別爲每一個維度 BIT 的個數,這裏再也不贅述。
#include <bits/stdc++.h> using namespace std; // 模板代碼省略 using ll = long long; int n, m, q; const int maxn = 5e3 + 50; BIT<ll> f[maxn]; // 二維BIT void add(int i, int j, ll x){ while (i <= n){ f[i].add(j, x); i += lowbit(i); } } ll sum(int i, int j){ ll res(0); while (i > 0){ res += f[i].sum(j); i -= lowbit(i); } return res; } signed main(){ ios::sync_with_stdio(0); cin.tie(0); cin >> n >> m; for (int i = 1; i <= n; ++ i) f[i] = BIT<ll>(m); int type; while (cin >> type){ if (type == 1){ int x, y, k; cin >> x >> y >> k; add(x, y, (ll) k); }else { int a, b, c, d; cin >> a >> b >> c >> d; cout << sum(c, d) - sum(c, b - 1) - sum(a - 1, d) + sum(a - 1, b - 1) << '\n'; } } return 0; }
這是我耗時最長的一篇博客,也是我花費心血最多的一次,也但願本身能好好掌握 BIT
附上參考連接: