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