前言
在介绍树状数组之前,需要了解一个函数lowbit(),其是求一个数的二进制最低次幂的值,这个函数是自己手写的,不是编译器自带的。例如,5的二进制表示为101,那么lowbit(5)就是101的最后一位,即1;10的二进制表示为1010,那么lowbit(10)就是1010的最后两位,即10。
接下来介绍lowbit函数的实现原理。以0110(设为x)为例,将其取反再加一,结果为1010(由于在计算机系统中,负数是由补码表示的,而补码就是由正数的原码取反再加一得来,故1010等于-x),0110 & 1010 = 0010,其就是0110的二进制最低次幂的值。所以x & -x ,最终的结果就是该数的二进制最低次幂的值。
int lowbit(int x) {
return x & -x;
}
下面进入正题,正式开始介绍树状数组。树状数组常用来加速原始逻辑数组(逻辑上的数组,而非0 1逻辑数组,实际操作是对树状数组本身)的区间求和。树状数组维护的区间是[1,r],而非[0,r]
定义
- 第i个位置记录(i - lowbit(i), i]中的数字的和。这一点就是和普通数组最大的不同,普通数组第i个位置表示的就是第i个数。
- 第i个位置的父节点是i + lowbit(i)
对定义的实例解释:第4个位置记录的是(4 - 4, 4],即[1,4]范围内数字的和,第6个位置记录的是[5, 6]范围数字的和;第5个位置的父节点为 5 + 1 = 6,即6是5的父节点。正是由于这样的结构,使得其名字为“树状数组”。
性质
- 对于某一个结点i,其所有子节点记录的区间不会相互覆盖,且按照从小到大的顺序依次覆盖(i - lowbit(i), i]。例如,对于结点8,其所有子节点为4,6,7,区间分别为[1,4] [5,6] [7,7]。
- 对于某一个结点i,仅会被i和它的祖先节点所覆盖。例如,结点2仅仅被结点2,4,8所覆盖。
查询操作
这一步也是和普通数组有着显著的区别。普通数组是直接暴力累加,树状数组是分步累加。
目的:查询树状数组的[1,r]范围内所有数字的和
原理:以结点7为例,其二进制表示为111,有三个1,故要累加三次,[1,7]的和被分解为[1,4] [5,6] [7,7]这三者的和。时间复杂度为log(n),因为n的二进制表示有log(n)个数字,而这些数字中最多有log(n)个1,故最坏情况下的复杂度为log(n)。
int query(int r) { // 查询[1,r]范围内所有数字的和
int ans = 0;
while (r != 0) {
ans += d[r];
r -= lowbit(r);
}
return ans;
}
修改操作
目的:给数组x位置上的数加上y
原理:由性质2,对于某一个结点i,其仅会被i和它的祖先节点所覆盖,即i和它的祖先包含了i。
故要对它们全部都加上y。时间复杂度为log(n)。
void alter(int x, int y) { // 在x位置上的数加上y
while (x <= n) {
d[x] += y;
x += lowbit(x);
}
}
实例练习
直接暴力,修改操作的复杂度为O(1),查询操作的复杂度为O(n),整体最坏的时间复杂度为O(n*m),会超时。使用树状数组这种数据结构,会使得修改和查询的复杂度得到均衡,使得整体的复杂度降到 log(n)。
#include<bits/stdc++.h>
using namespace std;
const int N = 5e5 + 10;
int d[N];
int n, m, num;
int lowbit(int x) {
return x & -x;
}
int query(int r) { // 查询[1,r]范围内所有数字的和
int ans = 0;
while (r != 0) {
ans += d[r];
r -= lowbit(r);
}
return ans;
}
void alter(int x, int y) { // 在x位置上的数加上y
while (x <= n) {
d[x] += y;
x += lowbit(x);
}
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> num;
alter(i, num);
}
int a, b, c;
for (int i = 1; i <= m; i++) {
cin >> a >> b >> c;
if (a == 1) {
alter(b, c);
} else if (a == 2) {
cout << query(c) - query(b-1) << endl;
}
}
return 0;
}