树状数组的相关知识 及 求逆序对的运用
文章目录
- 树状数组概念
- 前缀和和区间和
- 树状数组原理
- 区间和——单点更新
- 前缀和——区间查询
- 完整代码
- 离散化
- 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]
。避免了下标越界的风险。
举例为证:
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。 因此 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 }
,通过树状数组求逆序对的步骤如下:
- 创建长度为
100
的树状数组,下标从1
开始。 - 倒序遍历
ivec
,通过区域求和得到tree
数组中下标ivec[i]
的前缀和,前缀和代表着比ivec[i]
小的元素有几个。 - 更新单点,执行
tree[ivec[i]]++
。举例:ivec[i]=7
,tree[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
*/
树状数组的相关知识 及 求逆序对的运用相关推荐
- 【dfs序+树状数组】多次更新+求结点子树和操作,牛客小白月赛24 I题 求和
前置知识点 dfs遍历 树状数组/线段树知识 链接 I题 求和. 题意 已知有 n 个节点,有 n−1 条边,形成一个树的结构. 给定一个根节点 k,每个节点都有一个权值,节点i的权值为 vi 给 m ...
- 线段树 树状数组 离散化相关例题
文章目录 A-敌兵布阵 B-Lost Cows G-Buy Ticket E-l Hate It D-Just a Hook F-Ultra-Quicksort H-Stars C-Major's p ...
- 1010 Lehmer Code (35 分)(思路+详解+树状数组的学习+逆序对+map+vector) 超级详细 Come baby!!!
一:题目 According to Wikipedia: "In mathematics and in particular in combinatorics, the Lehmer cod ...
- 用二叉树来理解树状数组
树状数组(Fenwick tree,又名binary indexed tree),是一种很实用的数据结构.它通过用节点i,记录数组下标在[ i –2^k + 1, i]这段区间的所有数的信息(其中,k ...
- 【模板】一维树状数组
ACM模板 目录 聊聊前缀和 什么是树状数组? 树状数组相关操作 局限性 差分在树状数组中的应用 区间更新.单点查询 区间更新.区间查询 树状数组应用 聊聊前缀和 比如数组 int a[7]={1,2 ...
- 青出于蓝 dfs序+树状数组
题目来源:蓝桥杯2018模拟 武当派一共有 n 人,门派内 n 人按照武功高低进行排名,武功最高的人排名第 1,次高的人排名第 2,... 武功最低的人排名第 n.现在我们用武功的排名来给每个人标号, ...
- zoj 2112 树状数组 套主席树 动态求区间 第k个数
总算是把动态求区间第k个数的算法看明白了. 在主席树的基础上,如果有修改操作,则要通过套树状数组来实现任意区间求第k小的问题. 刚开始看不明白什么意思,现在有一点理解.树状数组的每个元素是一个线段树, ...
- 树状数组维护区间和的模型及其拓广的简单总结
by wyl8899 树状数组的基本知识已经被讲到烂了,我就不多说了,下面直接给出基本操作的代码. 假定原数组为a[1..n],树状数组b[1..n],考虑灵活性的需要,代码使用int *a传数组. ...
- 树状数组再进阶(区间修改+区间查询)
今天,我们再在树状数组上进一步突破,我们来讲一讲区间修改和区间查询.同样的,这需要各位对树状数组的基本知识有所了解,大家可以看看我的另一篇文章:树状数组趣解. 下面进入正题. 同样的,我还是先给代码, ...
最新文章
- 142页ICML会议强化学习笔记整理,值得细读
- 一文带你深入拆解Java虚拟机
- php javascript wav波形绘制,PHP分析.wav文件并绘制png格式的波形图
- [转载] 中华典故故事(孙刚)——13 马虎
- 《江湖X:汉家江湖》游戏论剑系统技术全解析
- Docker存储驱动之OverlayFS简介
- 关于static变量的定义及性质的深层介绍
- OpenGL样板程序,会转动的正方形
- 深入理解 JVM Class文件格式(四)
- java堆分析神器MAT
- Hadoop 的核心(1)—— HDFS
- 问题四十七:怎么用ray tracing画superellipsoid (2)
- 再也不怕重装eclipse! 让你的eclipse插件只下载一次
- 安川机器人dx200编程手册_安川DX100DX200通信接口手册
- 传输线理论 特征阻抗
- Binder机制原理
- Struts2通配符接收参数
- 【前端进阶】-TypeScript类型声明文件详解及使用说明
- Windows子系统(GUI)
- 【使用Modern UI快速开发WPF应用】
热门文章
- JAVA_if或者怎么用,Java If语句
- mysql pmm 布署_给 mysql 安装 pmm 监控
- Linux下,sqlite简单实例
- asterisk1.8 拨号方案 mysql存储(动态)
- AM335X 分配大于4M的framebuffer
- VOIP,PSTN,ISDN
- 错误./hello: error while loading shared libraries: libQtGui.so.4: cannot open shared object file:
- 设备I/O之OVERLAPPED
- linux 修改Db2主机名,修改DB2服务器的主机名
- 第十五节:深入理解async和await的作用及各种适用场景和用法