树状数组的相关知识 及 求逆序对的运用
文章目錄
- 樹狀數組概念
- 前綴和和區間和
- 樹狀數組原理
- 區間和——單點更新
- 前綴和——區間查詢
- 完整代碼
- 離散化
- sort函數
- unique函數去重
- erase函數僅保留不重復元素
- 通過樹狀數組求逆序對
樹狀數組概念
樹狀數組又名二叉索引樹,其查詢與插入的復雜度都為 O(logN),其具有以下特征:
前綴和和區間和
既然樹狀數組是為了解決前綴和問題,那么我們首先要知道什么是前綴和?
要提前綴和就不得不提區間和,舉個例子來說明兩者:
ivec = {1, 2, 3, 4} presum = {1, 3, 6, 10} // 前綴和 sumrange[1,3] = 9 // 下標1~3的區間和,2+3+4=9由上可得,sumrange[beg, end] = presum[end] - presum[beg - 1] ,以例子來分析其合理性:
因為 sumrange[1,3] = 2+3+4 ,presum[3] = 1+2+3+4 ,也就是說 sumrange[1,3] = presum[3] - ivec[0] ,ivec[0] = presum[0] = presum[1-1] ,因此, sumrange[1,3] = presum[3] + presum[0] 。
但 sumrange[beg, end] = presum[end] - presum[beg - 1] 有個隱患——訪問 beg-1 的位置容易導致下標越界,如:sumrange[0,4] 。因此我們可以改變前綴和數組下標 i 保存的內容,當有訪問越界風險時,前綴和數組下標 i 保存的是 [0, i] 的累加和;那么如果令 前綴和數組下標 i 保存 [0, i) 的累加和 ,令 presum[0] = 0 ,則可得到 sumrange[beg, end] = presum[end+1] - presum[beg] 。避免了下標越界的風險。
舉例為證:
明晰了如何通過前綴和數組來算區間和,那么實際上樹狀數組實現的就是如何用區間和算前綴和。
樹狀數組原理
樹狀數組本質上是 空間換時間 的操作,保存 區間和 以求更快的算出 前綴和。以下圖為例,紅色數組為樹狀數組(稱為C),藍色數組為普通數組(稱為A)。由于上面證明了從 1 開始存儲可以避免訪問越界的情況。另,也因為在計算前綴和時,終止條件通常為遇0。 因此 A 和 C 都是從 1 開始存儲元素。
區間和——單點更新
樹狀數組是如何保存 區間和 的呢?通過觀察上圖,我們可以得到如下規律:
C1 = A1 = sumrange[1]
C2 = C1 + A2 = A1 + A2 = sumrange[1, 2]
C3 = A3 = sumrange[3]
C4 = C2 + C3 + A4 = A1 + A2 + A3 + A4 = sumrange[1, 4]
C5 = A5 = sumrange[5]
C6 = C5 + A6 = A5 + A6 = sumrange[5, 6]
C7 = A7 = sumrange[7]
C8 = C4 + C6 + C7 + A8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8 = sumrange[1, 8]
以上規律可以總結歸納為這樣的特征:下標 i 存儲了從 i 往前 2k (k為二進制表示的 i 中 末尾0 的個數)個元素的區間和(出現次數),舉例驗證:
i = 8 = 1000, k = 3, 2^3 = 8, C8 是 A1~A8 的區間和(出現次數) i = 6 = 110, k = 1, 2^1 = 2, C6 是 A5~A6 的區間和(出現次數) i = 5 = 101, k = 0, 2^0 = 1, C5 是 A5 的區間和(出現次數)怎樣實現這樣的存儲方式呢?對于一個輸入的數組A,我們每一次讀取的過程,其實就是一個不斷更新單點值的過程,一邊讀入 A[i] ,一邊將 C[i] 涉及到的祖先節點值更新,完成輸入后樹狀數組也就建立成功了。舉個例子:
假設更新 A[2] = 8 ,那么管轄 A[2] 的 C[2],C[4],C[8] 都要加上 8(A2 的所有祖先節點),那么怎么找到所有的祖先節點呢?通過觀察他們的二進制形式我們發現:
- C2 = C10 ; C4 = C100 ; C8 = C1000
不明顯,再觀察一個一個例子,A[5] 的祖先節點有 C[5],C[6],C[8] ,觀察其二進制形式:
- C5 = C101 ; C6 = C110 ; C8 = C1000
也就是說,我們不斷地對 二進制i 的 末尾1 進行 +1 操作(尋找末尾1由Lowbit函數實現),直至到達 樹狀數組下標最大值 n 。
實現單點更新update(i, v):把下標 i 位置的數加上一個值 v 。
int Lowbit(int x){return x & -x; }void update(int i, int v){while(i<=n){ // n為樹狀數組.size()-1tree[i] += v;i += Lowbit(i);} }PS:在求逆序對的題目中,C[i] 保存某一區間元素出現的次數,便于快速計算前綴和。
前綴和——區間查詢
如何通過 區間和 得到 前綴和 ?舉例說明:
- presum[8] = C8 。8 = 1000
- presum[7] = C7 + C6 + C4 。7 = 111 ,6 = 110 ,4 = 100
- presum[5] = C5 + C4 。5 = 101 ,4 = 100
對于 presum[i] 而言,結合著后面跟的二進制表示,不難發現,求 presum[i] 即是將 i 轉換為 二進制 ,不斷對 末尾的1 進行 -1 的操作(尋找末尾1由Lowbit函數實現),直到全部為0停止。
?
實現區間查詢函數 query(i): 查詢序列 [1?i] 區間的區間和,即 i 位置的前綴和。
PS:在求逆序對的題目中,i-1 位的前綴和 presum[i-1] 表示「有多少個數比 i 小」,也就代表了有多少個逆序對。
int query(int i){int res = 0;while(i > 0){res += tree[i];i -= Lowbit(i);}return res; }完整代碼
class BIT {vector<int> tree;int len; public:BIT(int n):len(n), tree(n){}BIT(vector<int>& nums>{len = nums.size();tree = vector<int>(nums.size()+1);} static int Lowbit(int x){return x & -x;}int query(int x){ // 區間查詢int res = 0;while(x){res += tree[x];x -= Lowbit(x);}return res;}void update(int x){ // 單點更新while(x<len){tree[x]++;x += Lowbit(x);}} };離散化
離散化常常用在通過樹狀數組求逆序對的題目中,連續化時,樹狀數組的長度為普通數組的最大元素。
比如題目給出一個數組 ivec = { 7, 4, 5, 100, 7, 5 } ,通過樹狀數組求逆序對的步驟如下:
具體執行:
res = 0; // 存儲逆序對個數 ivec = { 7, 4, 5, 100, 7, 5 }^ 0 0 0 0 1 1 0 1 0 …… 0 1 2 3 4 5 6 7 8 9 …… 100 執行 res += query(4) 【已有的小于ivec[i]的元素才構成逆序對,因此從 ivec[i]-1 開始區間查詢】得到 res = 0 + 0 = 0 單點更新,下標為 5、6、8…… 的 value 加 1ivec = { 7, 4, 5, 100, 7, 5 }^ 0 0 0 0 1 1 1 2 0 …… 0 1 2 3 4 5 6 7 8 9 …… 100 執行 res += query(6) 得到 res = 0 + 1 = 1 單點更新,下標為 7、8…… 的 value 加 1ivec = { 7, 4, 5, 100, 7, 5 }^ 0 0 0 0 1 1 1 2 0 …… 1 1 2 3 4 5 6 7 8 9 …… 100 執行 res += query(99) 得到 res = 1 + 1 = 2 單點更新,下標為 100 的 value 加 1ivec = { 7, 4, 5, 100, 7, 5 }^ 0 0 0 0 2 2 1 3 0 …… 1 1 2 3 4 5 6 7 8 9 …… 100 執行 res += query(4) 得到 res = 2 + 0 = 2 單點更新,下標為 5、6、8…… 的 value 加 1以此類推,很容易算出逆序對的數量。但是!可以發現1、2、3、6、8、9、…… 、98、99這些絕大多數位置都浪費了。因此我們需要對樹狀數組離散化,以節省內存空間。
實現樹狀數組離散化:
void Discretization(vector<int>& nums) {// nums 是 輸入數組 的拷貝數組sort(nums.begin(), nums.end());nums.erase(unique(nums.begin(), nums.end()), nums.end()); //元素去重,下文有詳細剖析 }int getid(int x, vector<int> nums){return lower_bound(nums.begin(), nums.end(), x) - nums.begin() + 1; }上述代碼的作用簡單來講就是,通過 Discretization函數 將 nums 中的值保存到 a 中,并進行升序排列、元素去重的操作。以 ivec 為例,經過 Discretization函數 處理,得到
a = {4, 5, 7, 100}而通過 getid函數 將 a 中元素映射為對應的樹狀數組下標,也就是 4 存在樹狀數組下標為 1 的地方,5 存在樹狀數組下標為 2 的地方……以此類推。舉例:
ivec = { 7, 4, 5, 100, 7, 5 }^ 0 1 0 1 // value 4 5 7 100 // 映射得到的邏輯下標 1 2 3 4// 物理下標 執行 res += query(getid(5)) 得到 res = 0 單點更新,下標為 getid(5)=2、getid(100)=4 的 value 加 1ivec = { 7, 4, 5, 100, 7, 5 }^ 0 1 1 2 // value 4 5 7 100 // 映射得到的邏輯下標 1 2 3 4// 物理下標 執行 res += query(getid(7)) 得到 res = 1 單點更新,下標為 getid(7)=3、getid(100)=4 的 value 加 1下面是對 Discretization函數 的剖析。
sort函數
- 接受兩個迭代器,表示要排序的元素范圍
- 是利用元素類型的<運算符實現排序的,即默認升序
實例:
unique函數去重
- 重排輸入序列,將相鄰的重復項“消除”;
- “消除”實際上是把重復的元素都放在序列尾部,然后返回一個指向不重復范圍末尾的迭代器。
實例:
從上圖可知,unique返回的迭代器對應的vc下標為4,vc的大小并未改變,仍有10個元素,但順序發生了變化,相鄰的重復元素被不重復元素覆蓋了, 原序列中的“1 2 2”被“2 3 4”覆蓋,不重復元素出現在序列開始部分。
erase函數僅保留不重復元素
可以通過使用容器操作——erase刪除從end_unique開始直至容器末尾的范圍內的所有元素:
通過樹狀數組求逆序對
題源力扣:數組中的逆序對
代碼實現:
class BIT {vector<int> tree;int st; public:BIT(int n) :st(n), tree(n) {}BIT(vector<int>& nums) {st = nums.size();tree = vector<int>(nums.size());for (int i = 0; i < nums.size(); i++) {update(i, nums[i]);}}static int Lowbit(int x) {return x & -x;}int query(int x) { // 區間查詢int res = 0;while (x) {res += tree[x];x -= Lowbit(x);}return res;}void update(int x, int v) { // 單點更新while (x < st) {tree[x] += v;x += Lowbit(x);}}void show() {for (int i : tree) {cout << i << " ";}cout << endl;cout << " 4 5 7 100" << endl;} }; class Solution {void Discretization(vector<int>& tmp) {sort(tmp.begin(), tmp.end());tmp.erase(unique(tmp.begin(), tmp.end()), tmp.end()); //元素去重}int getid(int x, vector<int>& tmp) {return lower_bound(tmp.begin(), tmp.end(), x) - tmp.begin() + 1;} public:int reversePairs(vector<int>& nums) {int n = nums.size();vector<int> tmp = nums; // tmp作為離散化數組Discretization(tmp); // 排序去重BIT bit(tmp.size()+1);//bit.show();int res = 0; // 逆序對個數for (int i = n - 1; i >= 0; i--) {//cout << "v[i]: " << nums[i] << endl;int id = getid(nums[i], tmp); // 尋找映射res += bit.query(id - 1);// 因為計算的是value小于nums[i]元素的數目// 因此從前一位開始,下標id保存的是當前value=nums[i]的個數bit.update(id, 1); // nums[i]的個數+1//bit.show();//cout << "res: " << res << endl;}return res;} };int main() {vector<int> v = { 7, 4, 5, 100, 7, 5 };Solution s;/*int res = s.reversePairs(v);cout << res << endl;*/cout << s.reversePairs(v) << endl; } /* 7, 4, 5, 100, 7, 5 */總結
以上是生活随笔為你收集整理的树状数组的相关知识 及 求逆序对的运用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: leetcode574. 当选者(SQL
- 下一篇: leetcode1103. 分糖果 II