0x40进阶数据结构-(3)-树状数组
树状数组
树状数组(Binary Index Tree / Fenwick Tree)是一种简单的数据结构,主要用来维护序列的前缀和。
它根据任意正整数关于2的不重复次幂的唯一分解性质,把区间二进制分解,支持在对原序列单点更新,同时支持在内查询前缀和。
任意正整数关于2的不重复次幂的唯一分解:
就是说:如果我们想查询区间[1, x],可以先把x进行二进制分解:
假设正整数x的二进制表示为,其中等于1的位是,则正整数x可以被二进制分解成:那么查询区间[1,x],可以分解成查询以下log(x)个子区间:
长度为的子区间
长度为的子区间
…
长度为的子区间
这些子区间的共同特点是:若区间结尾为R,则区间长度就等于R的二进制分解下最小的2的次幂,即
例如:,那么查询区间[1, 7]就可以划分为查询、、三个不重复子区间,
区间长度分别是,,。
对于一个长度为N的序列,我们建立一个同样长度为N的树状数组,其中保存序列的区间中所有数的和,即。树状数组结构如下图所示:
该结构满足以下性质:
- 每个内部节点保存以它为根的子树中所有节点的和
- 每个内部节点的子节点个数等于的位数
- 除树根外,每个内部节点的父节点是
- 树的深度是
单点更新(update)
单点更新就是把原序列中的一个数加上一个,同时正确维护树状数组所记录的区间和。
根据上面给出的树状数组树形结构和它的性质,不难发现只有及其所有祖先节点保存的区间和包含,
因此就像“爬树”一样,从开始每次下标寻找到祖先节点,逐一进行更新即可。
查询前缀和(query)
查询前缀和,即序列中第个数的和。
按照我们上述提出的方法,应该求出x的二进制表示的每个等于1的位,把分为个不重复子区间,而每个子区间的区间和都已经保存在树状数组中,把这些子区间的区间和加起来就能在 的时间内查询前缀和。
1-based树状数组板子如下:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 7;
struct BinaryIndexTree {
int tr[N];
int lowbit(int x) {
return x & -x;
}
void update(int x, int d) {
for (; x < N; x += lowbit(x)) tr[x] += d;
}
int query(int x) {
int res = 0;
for (; x; x -= lowbit(x)) res += tr[x];
return res;
}
};
int main() {
int n;
cin >> n;
int a[N];
BinaryIndexTree bit;
memset(bit.tr, 0, sizeof bit.tr);
for (int i = 1; i <= n; i++) {
cin >> a[i];
bit.update(i, a[i]); // 构建树状数组
}
int l, r;
cin >> l >> r;
cout << bit.query(r) - bit.query(l - 1); //查询区间和[l, r]
return 0;
}
个人认为树状数组是要比ST表好理解不少的。
复杂度分析
- 时间复杂度:
- 构建:,构建相当于是做n次单点更新
- 单点更新:
- 查询区间和:
- 空间复杂度:
拓展
树状数组维护最值
没想到吧~(∠・ω< )⌒☆,树状数组也能维护最值,简直是叹为观止!
单点更新(update)
单点更新操作与前缀和是一样的,只有及其所有祖先节点保存的区间和包含,逐一更新即可。
查询最值(query)
查询区间最值和查询前缀和操作不太一样:区间前缀和是用的前缀和减去的前缀和。
显然区间最值是没有这个性质的,所以只能换个思路:
由定义:表示的是,我们就要不断缩小使逼近
当时,完全覆盖,
当时,分离,让减使逼近,
写成递归形式:
int query(int l, int r) {
if (l > r) return 0;
int res = 0;
if (r - lowbit(r) + 1 >= l) res = max(query(l, r - lowbit(r)), tr[r]);
if (r - lowbit(r) + 1 < l) res = max(query(l, r - 1), a[r]);
return res;
}
写成递推形式:
int query(int l, int r) {
int res = 0;
while (l <= r) {
res = max(res, a[r]); r--;
for (; r - lowbit(r) >= l; r -= lowbit(r)) res = max(res, tr[r]);
}
return res;
}
- 时间复杂度:,因为经过次变换后,其与不同的最高位至少会下降1位,所以最多进行次变换
下面给出树状数组维护区间最值的板子:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 7;
struct BinaryIndexTree {
int tr[N], a[N];
int lowbit(int x) {
return x & -x;
}
void update(int x, int v) {
for (; x < N; x += lowbit(x)) tr[x] = max(tr[x], v);
}
int query(int l, int r) {
int res = 0;
while (l <= r) {
res = max(res, a[r]); r--;
for (; r - lowbit(r) >= l; r -= lowbit(r)) res = max(res, tr[r]);
}
return res;
}
};
int main() {
int n;
cin >> n;
BinaryIndexTree bit;
memset(bit.tr, 0, sizeof bit.tr);
memset(bit.a, 0, sizeof bit.a);
for (int i = 1; i <= n; i++) {
cin >> bit.a[i];
bit.update(i, bit.a[i]); // 构建树状数组
}
int l, r;
cin >> l >> r;
cout << bit.query(l, r); //查询区间[l, r]的最值
return 0;
}
差分树状数组
todo:差分树状数组,可用于区间更新+单点查询
练习eg:
本博客所有文章均采用 CC BY-NC-SA 4.0 协议 ,禁止商用,转载请注明出处!