一、树状数组原理
树状数组主要运用于查询任意两点所有元素之和和更改任意一个元素的值
可以看到上图每个节点都管辖了一个范围
eg.节点2管辖了1 ~ 2这个区间,10管辖了9 ~ 10这个区间
那么决定每个节点管辖区间范围的因素是什么呢:不难发现,12转化为二进制后为1100,末尾有两个零,而且12管辖范围为4,即2^2,所以我们可以推测:
设节点编号为x,那么这个节点管辖的区间为2^k(其中k为x二进制末尾0的个数)个元素。
经验证后节点管辖区间范围的确满足上文所说
那么它是如何实现查询任意两点间的所有元素和的呢?
首先我们需要明白:假设有一个数列为4,10,20,55,89,现在我们要查询10到55间的所有元素和(包括10和55),那么我们只需用第4位数的前缀和减去第1位数的前缀和即可,
所以区间和问题就转化为了前缀和问题。
问题又出现了:维护前缀和也是一个不小的工作量啊,特别是加上了任意更改元素的值这一问题后,那么树状数组是怎么实现的呢?
这里又用上图举个栗子:
我们要求第13位元素的前缀和,在树状数组中只用把13, 9 ~ 12和1 ~ 8三个区间加起来,证明如下:
13的二进制为1101,管辖区间2^0 = 1,即13本身
12的二进制为1100,管辖区间2^2 = 4,即9 ~ 12的和
8的二进制为1000,管辖区间2^3 = 8,即1 ~ 8的和
发现查询一个元素的前缀和只需将这个数转化为二进制后加上每次删去最后一个0所返回的节点值(节点所对应的管辖区间的和)
这样我们每次求前缀和所要加的数就减少了许多,节省了许多时间,同时在需要对其中一个元素进行更改时就不用把所有前缀和都遍历一遍了,这就是树状数组的独特之处。
经过以上分析,我们也不难发现:
树状数组的单次查询时间复杂度为O(logn)
(每次删除二进制数的一个最后的0,最多删除logn次)
二、代码实现
1、lowbit
因为上文说过我们必须删除二进制最后的0来得到节点值,所以这里我们引入了lowbit操作:
lowbit操作可快速找到修改某个元素后所影响的其他的数的下标,具体操作如下:
int lowbit(int x) {
return x & (-x);
}
现在来解释一波: -x 便是求 x 的反码 +1 ,& 便是按位与符号,这里再举个例子:
以13为例,13的二进制为1101,求反码+1便为1110,再与1101求按位与则是1100,成功将最后的一个0删去,转化为二进制为12,即代表求13的前缀和要加上12这个节点所对应的管辖范围值,由此递推。
三、利用树状数组插入元素
利用上文的lowbit,我们修改被影响的数便可方便许多,代码如下:
void addval(int i, int val) {
while(i <= N) {
a[i] += val;
i += lowbit(i);
}
}
addval不用解释,i是我们想插入元素的位置,val便是插入元素的值,while循环遍历从插入位置开始到结束的所有区间并在循环里进行改变。(因为这里的数组为树状的前缀和数组,所以只用修改lowbit访问到的位置并赋值)
三、利用树状数组求和
同样用到了lowbit,求和便会简单许多,面对多组数据,它的时间复杂度也只是O(nlogn) * 询问次数,代码:
int summ(int i) {
int sum = 0;
while(i > 0) {
sum += a[i];
i -= lowbit(i);
}
return sum;
}
四、举个简单的例子
请移步至洛谷P3374
这里就不跟大家讲了,直接上代码:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 5e5 + 10;
int a[MAXN];
int N, M;
int lowbit(int x) {
return x & (-x);
}
void addval(int i, int val) {
while(i <= N) {
a[i] += val;
i += lowbit(i);
}
}
int summ(int i) {
int sum = 0;
while(i > 0) {
sum += a[i];
i -= lowbit(i);
}
return sum;
}
int main() {
cin >> N >> M;
for(int i = 1; i <= N; i++) {
int yy; cin >> yy;
addval(i, yy);
}
for(int i = 1; i <= M; i++) {
int m, x, k;
cin >> m >> x >> k;
if(m == 1) addval(x, k);
else {
cout << summ(k) - summ(x-1) << "\n";//求任意区间和
}
}
return 0;
}