树状数组
树状数组是一种可以“动态求区间和”的树形数据结构,但并没有真正构造出边和树来,所以是“树状”的。
之前基础算法中有学习过前缀和也可以用来求区间和,但是对于修改后再去求前缀和而言复杂度是O(n)的,而树状数组是log(n)的,这就是对于动态求区间和的一种结构
基础树状数组可以实现对区间和的单点修改(log(n),和树状数组结构相关)和区间查询(log(n)),树状数组所需要的东西非常简单,就一个数组t[N],大小和我们维护的数组大小一样即可
树状数组结构: t i t_i ti = ∑ j = l w o b i t ( i ) + 1 i a j \sum_{j=lwobit(i)+1}^{i} a_j ∑j=lwobit(i)+1iaj
t[i]存储a[]数组中的一段区间的和。定义t[i]存储以i结尾,且区间大小为lowbit(i)的区间的和。即t[i]表示a数组从下标[i-lowbit(i)+1 ~ i]的范围之和。
什么是lowbit(i)?,lowbit(i)表示i的二进制最低位1的数的大小。
举个例子:二进制串lowbit(001010100)就是000000100,lowbit(101001)就是000001。利用二进制特性,可以通过i & -i求得i的最低位1表示的数的大小(因为二进制在计算机中以补码形式存在,-i即i二进制取反加一,例如100100, 取反后变成011011,加一后变成011100,&是位运算操作,1&1 = 1,0 & 1= 0, 0 & 0 = 0。取反加一后可以发现最低位1及其右边所有位数都不变,但是左边全都变成了相反0变1,1变0,&上之后自然只剩下最低位1了。
- 熟悉了t数组含义后,我们就来看一下修改和求和这两个操作
单点修改update:
看上图,假如我们修改a[3],让它加上x,我们需要维护t数组中那些值呢,可以看出我们应该修改t3, t4, t8这三个点,因为这三个点的管辖区间都包含3这个节点。
如何去找到t3, t4, t8这三个点,就需要用到刚刚的lowbit操作,这里通过+lowbit操作:
假设修改单点3:
3 + lowbit(3) = 4
4 + lowbit(4) = 8
…
区间查询getprefix:
对于[L, R],查询该区间的和:可以转化为[1, R] - [1, L-1]的差,类似于前缀和得到一段区间和写法。
那如何得到[1, R]的区间和呢,就是-lowbit操作:
假设求sum[1, 5]:
5 - lowbit(5) = 4
4 - lowbit(4) = 0;
到0就终止
这就是为什么树状数组这两个操作都是log(n)的了,因为需要维护t
这里给出这两个操作的具体函数:
//首先是lowbit,就一行
int lowbit(int x) {
return x & -x;
}
//update k位置增加x
void update(int k, int x) {
for(int i = k; i <= n; i += lowbit(i)) {
t[i] += x;
}
}
//getprefix 求sum[1, k]
int getprefix(int k) {
int ans = 0;
for(int i = k; i > 0; i -= lowbit(i))
ans += t[i];
return ans;
}
例题:
一、P3374 【模板】树状数组 1 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
#include<iostream> #include<vector> using namespace std; const int N = 5e5 + 10; vector<int> a(N), t(N); int n, m; int lowbit(int x) { return x & -x; } void update(int k, int x) { // 在k位置上a[k]加上x for(int i = k; i <= n; i += lowbit(i)) t[i] += x; } int getprefix(int k) { int ans = 0; for(int i = k; i > 0; i -= lowbit(i)) ans += t[i]; return ans; } int main() { cin >> n >> m; for(int i = 1; i <= n; i++) { cin >> a[i]; update(i, a[i]); // 一开始a[i]数组都为0,每次输入一个数,相当与在原i位置添加一个a[i] } for(int i = 1; i <= m; i++) { int op, x, y; cin >> op >> x >> y; if(op == 1) update(x, y); //题目意思是在原基础上加上y else { int ans = getprefix(y) - getprefix(x-1); cout << ans << '\n'; } } return 0; }
P3368 【模板】树状数组 2 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
这个题和上面那个不同在于对于差分的考察,差分数组求一遍前缀和得到原数组
对于区间[x, y]内每个数都加上k,只需要修改差分数组中两个值,分别是x位置差分数组加k, y+1位置上减k
#include<iostream> #include<vector> using namespace std; using ll = long long; const int N = 5e5 + 10; vector<ll> a(N), dif(N), t(N); //这里的t数组里面相当于存的是a的差分数组dif的区间和 int n, m; int lowbit(int x) { return x & -x; } void update(int k, int x) { // 在k位置上dif[k]加上x for(int i = k; i <= n; i += lowbit(i)) t[i] += x; } ll getprefix(int k) { ll ans = 0; for(int i = k; i > 0; i -= lowbit(i)) ans += t[i]; return ans; } int main() { cin >> n >> m; for(int i = 1; i <= n; i++) cin >> a[i]; dif[0] = 0; for(int i = 1; i <= n; i++) { dif[i] = a[i] - a[i-1]; update(i, dif[i]); } for(int i = 1; i <= m; i++) { int op; cin >> op; if(op == 1) { int x, y, k; cin >> x >> y >> k; update(y+1, -k);// update(x, k); } else { int x; cin >> x; ll ans = getprefix(x); cout << ans << '\n'; } } return 0; }