树状数组
树状数组,顾名思义,就是树一样的数组(废话)。树状数组,又叫二叉索引树(Binary Indexed Tree),其发明者又称其为Fenwich Tree。其支持两种操作,时间复杂度均为
O
(
l
o
g
n
)
O(logn)
O(logn):单点修改和区间查询。
在最基础的应用中,单点修改便是修改数组中一个元素的值,区间查询便是查询数组一个区间的和。
例题
上述说到,树状数组最简单的应用中区间查询便是查询数组一个区间的和,那我们便以此来当做例题:
假设输入第一行一个数字T,为T组测试数据
每一组测试数据的第一行第一个正整数N(N<=50000),为数组长度。接下来,有N个正整数,第i个正整数为数组元素ai。
接下来每一行是一条命令,命令分为四种:
(1)A i j:数组i下标增加j。
(2)M i j:数组i下标减去j。
(3)Q i j:查询数组区间i到j的和
(4)E:结束
引入
对于普通数组而言,单点修改的时间复杂度为
O
(
1
)
O(1)
O(1),但区间求和的时间复杂度为
O
(
n
)
O(n)
O(n)。如果用前缀和的方法维护这个数组,下标i代表从数组开始到i的数的和,那么区间求和的时间复杂度便降到了
O
(
1
)
O(1)
O(1),但是单点修改会影响到后面所有的元素,所以单点修改的时间复杂度上升到了
O
(
n
)
O(n)
O(n)。
所以此时,最好采用树状数组,两个操作的时间复杂度折中了一下。树状数组的每一个元素,维护的都是一段区间的区间和。其实,普通数组的元素
i
i
i维护的是区间
[
a
i
,
a
i
]
[a_i, a_i]
[ai,ai]的区间和;前缀和数组
i
i
i维护的是区间
[
a
0
,
a
i
]
[a_0, a_i]
[a0,ai]的区间和。
树状数组每个元素维护的区间,是依靠二进制的。我们假设需要计算前11项的和。11的二进制为
(
1011
)
2
(1011)_2
(1011)2,那么前11项的和便分为区间
(
1010
,
1011
]
(1010, 1011]
(1010,1011]、
(
1000
,
1010
]
(1000, 1010]
(1000,1010]和
(
0000
,
1000
]
(0000, 1000]
(0000,1000]的和,这三个区间的和分别存储在树状数组下标1011、1010和1000中。这三个下标,便是每次不断去掉二进制的最后一个0。(1011去掉最后一个0,得到1010,1010去掉最后一个0,得到1000,再去掉,就是0000了。)
树状数组中定义一个数
x
x
x,它最后一个1连带着这个1后面的0,成为
l
o
w
b
i
t
(
x
)
lowbit(x)
lowbit(x)。树状数组为
C
C
C,
C
i
C_i
Ci保存着区间
(
a
i
−
l
o
w
b
i
t
(
a
i
)
,
a
i
]
(a_i - lowbit(a_i), a_i]
(ai−lowbit(ai),ai]的区间和。这样显然查询前n项和时需要合并的区间数是少于
l
o
g
2
n
log_2n
log2n的。大致结构如下图:
那么更新操作,就可能会更新多个节点,如下图:
我们再以更新二进制数 ( 100110 ) 2 (100110)_2 (100110)2为例,首先其肯定在区间 ( 100100 , 100110 ] (100100, 100110] (100100,100110]中,那么它肯定也在区间 ( 100000 , 101000 ] (100000, 101000] (100000,101000]中,那么它肯定也在区间 ( 100000 , 110000 ] (100000, 110000] (100000,110000]中,那么它肯定也在区间 ( 000000 , 100000 ] (000000, 100000] (000000,100000]中。可以发现,这些区间的右端点的变化有点像进位的过程,那么细心的便宜已经发现了,每次加的都是 l o w b i t ( x ) lowbit(x) lowbit(x)。这样,我们更新的区间数不会超过 l o g 2 l e n g t h log_2length log2length。
代码
单点修改
void update(int i, int x) {
for (int pos = i; pos < MAXN; pos += lowbit(pos)) {
tree[pos] += x;
}
}
求前n项和
int query(int x) {
int res = 0;
for (int pos = x; pos >= 1; pos -= lowbit(pos)) {
res += tree[pos];
}
return res;
}
求区间和
int query(int x, int y) {
return query(y) - query(x - 1);
}
整体代码
#include <iostream>
using namespace std;
const int MAXN = 50005;
int tree[MAXN]; //题目中说的N最大为50000,为了方便,申请全局变量数组
int lowbit(int x) { return x & (-x); }
void update(int i, int x) {
for (int pos = i; pos < MAXN; pos += lowbit(pos)) {
tree[pos] += x; //
}
}
int query(int x) {
int res = 0;
for (int pos = x; pos >= 1; pos -= lowbit(pos)) {
res += tree[pos];
}
return res;
}
int query(int x, int y) {
return query(y) - query(x - 1);
}
void main()
{
int T;
cin >> T;
for (int t = 0; t < T; ++t) {
memset(tree, 0, sizeof(tree));
int N, temp;
cin >> N;
for (int i = 1; i <= N; ++i) {
cin >> temp;
update(i, temp); //初始化的时候,就是更新每一个i位置的值。
}
char ch;
cout << "case: " << t + 1 << endl;
while (true) {
cin >> ch;
bool end = false;
int i, j;
switch (ch)
{
case 'E':
end = true;
break;
case 'A':
cin >> i >> j;
update(i, j);
break;
case 'M':
cin >> i >> j;
update(i, -j); //减法就是加负数
break;
case 'Q':
cin >> i >> j;
cout << "[" << i << ", " << j << "]: " << query(i, j) << endl;
}
if (end) {
break;
}
}
}
system("pause");
}
逆序对
当然,上述的只是树状数组最基本的应用。下面再介绍一个较为广泛的树状数组的应用,逆序对:
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
示例:
输入: [7,5,6,4]
输出: 5
代码如下:
class BIT{
private:
int n;
vector<int> tree;
public:
BIT(int _n):n(_n), tree(_n + 1){}
static int lowbit(int x){return x & (-x);}
void update(int i, int x){
for (int pos = i; pos < tree.size(); pos += lowbit(pos)){
tree[pos] += x;
}
}
int query(int i){
int res = 0;
for (int pos = i; pos >= 1; pos -= lowbit(pos)){
res += tree[pos];
}
return res;
}
};
class Solution {
public:
int reversePairs(vector<int>& nums) {
int n = nums.size();
vector<int> temp = nums;
sort(temp.begin(), temp.end());
for (int& num:nums){
// 由于数字可能比较多,这里采用离散化,因为在逆序对中,我们只关心元素的相对大小,所以可以先进行排序,找到其在排序中的位置,来节省空间
num = lower_bound(temp.begin(), temp.end(), num) - temp.begin() + 1;
}
BIT bit(n);
int ans = 0;
for (int i = n - 1; i >= 0; --i){
//因为后面的数大于前面的数才算逆序对,所以加就要查找已经出现的小于等于新nums[i]-1的数字的个数
ans += bit.query(nums[i] - 1);
bit.update(nums[i], 1);
}
return ans;
}
};
树状数组和线段树
树状数组和线段树都是一种擅长处理区间的数据结构。它们最大的区别之一就是线段树是一颗完美二叉树,而树状数组(BIT)相当于是线段树中每个节点的右儿子去掉。
用树状数组能够解决的问题,用线段树肯定能够解决,反之则不一定。但是树状数组有一个明显的好处就是较为节省空间,实现要比线段树要容易得多,而且在处理某些问题的时候使用树状数组效率反而会高得多。
另
水平有限,只介绍了两种最简单的应用。