文章目录

  • 树状数组概念
  • 前缀和和区间和
  • 树状数组原理
  • 区间和——单点更新
  • 前缀和——区间查询
  • 完整代码
  • 离散化
    • 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
单点更新,下标为 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
*/

树状数组的相关知识 及 求逆序对的运用相关推荐

  1. 【dfs序+树状数组】多次更新+求结点子树和操作,牛客小白月赛24 I题 求和

    前置知识点 dfs遍历 树状数组/线段树知识 链接 I题 求和. 题意 已知有 n 个节点,有 n−1 条边,形成一个树的结构. 给定一个根节点 k,每个节点都有一个权值,节点i的权值为 vi 给 m ...

  2. 线段树 树状数组 离散化相关例题

    文章目录 A-敌兵布阵 B-Lost Cows G-Buy Ticket E-l Hate It D-Just a Hook F-Ultra-Quicksort H-Stars C-Major's p ...

  3. 1010 Lehmer Code (35 分)(思路+详解+树状数组的学习+逆序对+map+vector) 超级详细 Come baby!!!

    一:题目 According to Wikipedia: "In mathematics and in particular in combinatorics, the Lehmer cod ...

  4. 用二叉树来理解树状数组

    树状数组(Fenwick tree,又名binary indexed tree),是一种很实用的数据结构.它通过用节点i,记录数组下标在[ i –2^k + 1, i]这段区间的所有数的信息(其中,k ...

  5. 【模板】一维树状数组

    ACM模板 目录 聊聊前缀和 什么是树状数组? 树状数组相关操作 局限性 差分在树状数组中的应用 区间更新.单点查询 区间更新.区间查询 树状数组应用 聊聊前缀和 比如数组 int a[7]={1,2 ...

  6. 青出于蓝 dfs序+树状数组

    题目来源:蓝桥杯2018模拟 武当派一共有 n 人,门派内 n 人按照武功高低进行排名,武功最高的人排名第 1,次高的人排名第 2,... 武功最低的人排名第 n.现在我们用武功的排名来给每个人标号, ...

  7. zoj 2112 树状数组 套主席树 动态求区间 第k个数

    总算是把动态求区间第k个数的算法看明白了. 在主席树的基础上,如果有修改操作,则要通过套树状数组来实现任意区间求第k小的问题. 刚开始看不明白什么意思,现在有一点理解.树状数组的每个元素是一个线段树, ...

  8. 树状数组维护区间和的模型及其拓广的简单总结

    by wyl8899 树状数组的基本知识已经被讲到烂了,我就不多说了,下面直接给出基本操作的代码. 假定原数组为a[1..n],树状数组b[1..n],考虑灵活性的需要,代码使用int *a传数组. ...

  9. 树状数组再进阶(区间修改+区间查询)

    今天,我们再在树状数组上进一步突破,我们来讲一讲区间修改和区间查询.同样的,这需要各位对树状数组的基本知识有所了解,大家可以看看我的另一篇文章:树状数组趣解. 下面进入正题. 同样的,我还是先给代码, ...

最新文章

  1. 142页ICML会议强化学习笔记整理,值得细读
  2. 一文带你深入拆解Java虚拟机
  3. php javascript wav波形绘制,PHP分析.wav文件并绘制png格式的波形图
  4. [转载] 中华典故故事(孙刚)——13 马虎
  5. 《江湖X:汉家江湖》游戏论剑系统技术全解析
  6. Docker存储驱动之OverlayFS简介
  7. 关于static变量的定义及性质的深层介绍
  8. OpenGL样板程序,会转动的正方形
  9. 深入理解 JVM Class文件格式(四)
  10. java堆分析神器MAT
  11. Hadoop 的核心(1)—— HDFS
  12. 问题四十七:怎么用ray tracing画superellipsoid (2)
  13. 再也不怕重装eclipse! 让你的eclipse插件只下载一次
  14. 安川机器人dx200编程手册_安川DX100DX200通信接口手册
  15. 传输线理论  特征阻抗
  16. Binder机制原理
  17. Struts2通配符接收参数
  18. 【前端进阶】-TypeScript类型声明文件详解及使用说明
  19. Windows子系统(GUI)
  20. 【使用Modern UI快速开发WPF应用】

热门文章

  1. JAVA_if或者怎么用,Java If语句
  2. mysql pmm 布署_给 mysql 安装 pmm 监控
  3. Linux下,sqlite简单实例
  4. asterisk1.8 拨号方案 mysql存储(动态)
  5. AM335X 分配大于4M的framebuffer
  6. VOIP,PSTN,ISDN
  7. 错误./hello: error while loading shared libraries: libQtGui.so.4: cannot open shared object file:
  8. 设备I/O之OVERLAPPED
  9. linux 修改Db2主机名,修改DB2服务器的主机名
  10. 第十五节:深入理解async和await的作用及各种适用场景和用法