樹狀數組的相關知識 及 求逆序對的運用

文章目錄

  • 樹狀數組概念
  • 前綴和和區間和
  • 樹狀數組原理
  • 區間和——單點更新
  • 前綴和——區間查詢
  • 完整代碼
  • 離散化
    • sort函數
    • unique函數去重
    • erase函數僅保留不重復元素
  • 通過樹狀數組求逆序對


樹狀數組概念

樹狀數組又名二叉索引樹,其查詢與插入的復雜度都為 O(logN),其具有以下特征:

  1. 樹狀數組是一種實現了高效查詢「前綴和」與「單點更新」操作的數據結構。
  2. 是求逆序對的經典做法。
  3. 不能解決數組有增加和修改的問題。

前綴和和區間和

既然樹狀數組是為了解決前綴和問題,那么我們首先要知道什么是前綴和?

要提前綴和就不得不提區間和,舉個例子來說明兩者:

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+4presum[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] 。避免了下標越界的風險。
舉例為證:

ivec = {1, 2, 3, 4}
presum = {0, 1, 3, 6, 10}
sumrange[1,3] = presum[3+1] - presum[1] = 10 - 1 = 9
sumrange[0,3] = presum[3+1] - presum[0] = 10 - 0 = 10

明晰了如何通過前綴和數組來算區間和,那么實際上樹狀數組實現的就是如何用區間和前綴和


樹狀數組原理

樹狀數組本質上是 空間換時間 的操作,保存 區間和 以求更快的算出 前綴和。以下圖為例,紅色數組為樹狀數組(稱為C),藍色數組為普通數組(稱為A)。由于上面證明了從 1 開始存儲可以避免訪問越界的情況。另,也因為在計算前綴和時,終止條件通常為遇0。 因此 AC 都是從 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] 都要加上 8A2 的所有祖先節點),那么怎么找到所有的祖先節點呢?通過觀察他們的二進制形式我們發現:

  • 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] = C88 = 1000
  • presum[7] = C7 + C6 + C47 = 1116 = 1104 = 100
  • presum[5] = C5 + C45 = 1014 = 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 } ,通過樹狀數組求逆序對的步驟如下:

  1. 創建長度為 100 的樹狀數組,下標從 1 開始。
  2. 倒序遍歷 ivec ,通過區域求和得到 tree 數組中下標 ivec[i] 的前綴和,前綴和代表著比 ivec[i] 小的元素有幾個。
  3. 更新單點,執行 tree[ivec[i]]++ 。舉例:ivec[i]=7tree[7]++ ,代表 7 已被遍歷過,出現了一次。

具體執行:

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
單點更新,下標為 568…… 的 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
單點更新,下標為 78…… 的 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
單點更新,下標為 568…… 的 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)=2getid(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)=3getid(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
*/

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/443783.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/443783.shtml
英文地址,請注明出處:http://en.pswp.cn/news/443783.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

二叉搜索樹相關知識及應用操作

文章目錄概念查找二叉搜索樹的第k大節點概念 二叉查找樹&#xff08;Binary Search Tree&#xff09;&#xff0c;&#xff08;又名&#xff1a;二叉搜索樹&#xff0c;二叉排序樹&#xff09;——它或者是一棵空樹&#xff0c;或者是具有下列性質的二叉樹&#xff1a; 若它的…

二叉樹相關知識及求深度的代碼實現

文章目錄樹二叉樹滿二叉樹和完全二叉樹二叉樹的性質代碼實現求二叉樹的深度樹 樹是一種非線性的數據結構&#xff0c;它是由n個有限結點組成一個具有層次關系的集合。 樹的相關名詞&#xff1a; 根節點&#xff1a;沒有前驅結點的結點。父節點&#xff0c;子節點&#xff1a…

平衡樹相關知識及如何判斷一棵樹是否平衡

文章目錄概念代碼實現判斷一棵二叉樹是否為平衡樹概念 平衡樹(Balance Tree&#xff0c;BT) 指的是&#xff0c;任意節點的子樹的高度差都小于等于1。 常見的符合平衡樹的有&#xff1a; B樹&#xff08;多路平衡搜索樹&#xff09;AVL樹&#xff08;二叉平衡搜索樹&#xf…

大端小端存儲模式詳解及判斷方法

文章目錄大小端模式的概念兩種模式出現原因兩種模式的優劣大小端的應用情景判斷機器的字節序大小端模式的概念 當我們查看數據在內存中的存儲情況時&#xff0c;我們經常會發現一個很奇怪的現象&#xff0c;什么現象呢&#xff1f; int main() {int i 12;return 0; }數據在內…

Linux 內存管理 | 物理內存、內存碎片、伙伴系統、SLAB分配器

文章目錄物理內存物理內存分配外部碎片內部碎片伙伴系統(buddy system)slab分配器物理內存 在Linux中&#xff0c;內核將物理內存劃分為三個區域。 在解釋DMA內存區域之前解釋一下什么是DMA&#xff1a; DMA&#xff08;直接存儲器訪問&#xff09; 使用物理地址訪問內存&am…

Linux 內存管理 | 虛擬內存管理:虛擬內存空間、虛擬內存分配

文章目錄虛擬地址空間用戶空間內核空間用戶空間內存分配malloc內核空間內存分配kmallocvmalloc虛擬地址空間 在早期的計算機中&#xff0c;程序是直接運行在物理內存上的&#xff0c;而直接使用物理內存&#xff0c;通常都會面臨以下幾種問題&#xff1a; 內存缺乏訪問控制&a…

Linux | 編譯原理、gcc的命令參數、自動化構建工具 make/Makefile

文章目錄編譯原理預處理編譯匯編鏈接gcc的常用命令參數make 和 Makefile 的概念make的運行通配符自動化變量偽目標.PHONE:【命令】編譯原理 在解釋 makefile 前&#xff0c;首先解釋一下 .c 文件變成 .exe 文件要經過的四個步驟——預處理、編譯、匯編和鏈接&#xff08;參考來…

全排列變種:限定 排列的差值范圍 及 排列中的元素個數

文章目錄題目描述思路代碼實現題目描述 詳細描述&#xff1a;字節跳動2019春招研發部分編程題——萬萬沒想到之抓捕孔連順 輸入描述&#xff1a; 第一行包含空格分隔的兩個數字 N和D(1?≤?N?≤?1000000; 1?≤?D?≤?1000000) 第二行包含N個整數&#xff08;取值區間為…

Linux | 進程概念、進程狀態(僵尸進程、孤兒進程、守護進程)、進程地址空間

文章目錄進程和程序操作系統如何控制和調度程序進程控制塊–PCB子進程進程狀態僵尸進程孤兒進程守護進程&#xff08;精靈進程&#xff09;進程地址空間引言頁表進程和程序 程序&#xff1a; 一系列有序的指令集合&#xff08;就是我們寫的代碼&#xff09;。進程&#xff1a;…

Linux 進程控制 :進程創建,進程終止,進程等待,程序替換

文章目錄進程創建進程等待程序替換進程終止進程創建 fork函數&#xff1a; 操作系統提供的創建新進程的方法&#xff0c;父進程通過調用 fork函數 創建一個子進程&#xff0c;父子進程代碼共享&#xff0c;數據獨有。 當調用 fork函數 時&#xff0c;通過 寫時拷貝技術 來拷貝…

Linux 內存管理 | 連續分配方式 和 離散分配方式

文章目錄前言連續分配單一連續分配分區式分配固定分區分配動態分區分配可重定位分區分配離散分配分段分頁多級頁表快表(TLB)段頁式Linux前言 Linux 內存管理 | 虛擬內存管理&#xff1a;虛擬內存空間、虛擬內存分配 Linux 內存管理 | 物理內存、內存碎片、伙伴系統、SLAB分配器…

操作系統 | 用戶態和內核態的切換(中斷、系統調用與過程(庫函數)調用)

文章目錄中斷過程調用系統調用過程調用和系統調用的區別中斷 用戶態、內核態之間的切換是怎么實現的? 用戶態→內核態 是通過中斷實現的。并且 中斷是唯一途徑 。核心態→用戶態 的切換是通過執行一個特權指令&#xff0c;將程序狀態字 (PSW) 的標志位設置為 用戶態 。 中斷…

管道實現父子進程的信息傳遞(二)【標準流和其文件描述符、fwrite函數、perror函數】

文章目錄代碼實現標準流 和 標準流文件描述符代碼中用到的函數fwrite()perror()在復習進程間的通信方式時又寫了一遍&#xff0c;和 管道實現父子進程的信息傳遞&#xff08;一&#xff09;【fork函數、pipe函數、write/read操作、wait函數】 的區別不是特別大&#xff0c;只是…

JAVA隨機生成文件名:當前年月日時分秒+五位隨機數

代碼如下&#xff1a; package cn.gov.csrc.util;import java.text.SimpleDateFormat; import java.util.Date; import java.util.Random;public class RandomUtil {/*** 生成隨機文件名&#xff1a;當前年月日時分秒五位隨機數* * return*/public static String getRandomFile…

命名管道實現進程的信息傳遞【mkfifo函數、open函數】

文章目錄代碼實現mkfifo函數open函數代碼實現 #include<fcntl.h> // open() #include<sys/wait.h> // wait() #include<sys/types.h> // mkfifo() #include<sys/stat.h> // mkfifo() #include<iostream> #include<unistd.h> // fork()usi…

Linux 進程 | 進程間的通信方式

文章目錄管道匿名管道 pipe命名管道 FIFO共享內存共享內存的使用流程&#xff1a;消息隊列信號量套接字在之前的博客中講過&#xff0c;虛擬空間出現的其中一個目的就是解決 進程沒有獨立性&#xff0c;可能訪問同一塊物理內存 的問題。因為這種獨立性&#xff0c;進程之間無法…

Linux網絡編程 | socket介紹、網絡字節序與主機字節序概念與兩者的轉換、TCP/UDP 連接中常用的 socket 接口

文章目錄套接字socket 地址通用 socket 地址專用 socket 地址網絡字節序與主機字節序地址轉換TCP/UDP 連接中常用的 socket 接口套接字 什么是套接字&#xff1f; 所謂 套接字 (Socket) &#xff0c;就是對網絡中 不同主機 上的應用進程之間進行雙向通信的端點的抽象。 UNIX/L…

網絡協議分析 | 傳輸層 :史上最全UDP、TCP協議詳解,一篇通~

文章目錄UDP概念格式UDP如何實現可靠傳輸基于UDP的應用層知名協議TCP概念格式保證TCP可靠性的八種機制確認應答、延時應答與捎帶應答超時重傳滑動窗口滑動窗口協議后退n協議選擇重傳協議流量控制擁塞控制發送窗口、接收窗口、擁塞窗口快速重傳和快速恢復連接管理機制三次握手連…

JDom,jdom解析xml文件

1.要解析的文件模板如下&#xff1a; <?xml version"1.0" encoding"GBK"?> <crsc> <data><舉報信息反饋><R index"1"><舉報編號>1</舉報編號><狀態>1</狀態><答復意見>填寫…

網絡協議分析 | 應用層:HTTP協議詳解、HTTP代理服務器

文章目錄概念URLHTTP協議的特點HTTP協議版本格式請求報文首行頭部空行正文響應報文首行頭部空行正文Cookie與SessionHTTP代理服務器正向代理服務器反向代理服務器透明代理服務器概念 先了解一下 因特網&#xff08;Internet&#xff09; 與 萬維網&#xff08;World Wide Web&…