整理:树状数组
关于树状数组的整理
1.何为树状数组?
\(OI\ Wiki\):
树状数组是一种支持 单点修改 和 区间查询 的,代码量小的数据结构。
说人话,就是好写的,但是适用范围窄的线段树/分块
2.树状数组与其他类似数据结构的对比
| 时间复杂度 | 空间复杂度 | 适用范围 | 码量 | |
|---|---|---|---|---|
| \(分块\) | \(O(n\sqrt n)\) | \(O(n\sqrt n)\) | 巨大 | 大 |
| \(树状数组\) | \(O(nlogn)(常数小)\) | \(O(n)\) | 小 | 小 |
| \(线段树\) | \(O(nlongn)(常数大)\) | \(O(n)\) | 大 | 巨大 |
3.树状数组的适用条件
\(OI\ Wiki\):
普通树状数组维护的信息及运算要满足 结合律 且 可差分,如加法(和),乘法(积),异或等
但是,在某些时候,树状数组可以尝试维护区间最值
比如:使用两个树状数组/使用拓展树状数组(时间复杂度为 \(O(nlog^2n)\))
4.树状数组的基本思想
就是将原本储存单个元素的数组变成储存一段区间信息的数组,以此提高程序效率,类似于倍增st表,但是支持修改
树状数组所组成的树实际上是 \(i\) 的父亲为 \(i+lowbit(i)\) 的树
这样的树就满足了很多有意思的性质
具体性质见\(OI\;Wiki\)
5.树状数组的基本实现
我们首先来实现单点修改和区间和查询.
对于修改位置 \(x\),我们可以知道,这个位置将影响到这个节点和树中所有这个节点的祖先,而我们又知道 \(i\) 的父亲是 \(i+lowbit(i)\),所以我们同时对所有受影响的区间进行修改即可
void add(int x,int v){
for(int i=x;i<=n;i+=lowbit(i))a[i]+=v;
}
然后我们来实现区间和查询
我们首先将 \([l,r]\) 的和转换成 \([1,r]\) 的和减去 \([1,l-1]\) 的和,也就变成了单点前缀和查询
又由于我们知道,对于 \(x\) 节点,它所维护的区间为 \([x-lowbit(x)+1,x]\),所以我们为了不重复统计,我们每次统计完之后,直接跳到下一个不交的区间即可
int query(int x){
int res=0;
for(int i=x;i;i-=lowbit(i))
res+=a[i];
return res;
}
int query(int l,int r){
return query(r)-query(l-1);
}
区间乘/异或的操作与此类似
那么如果我们进行区间加呢?
首先,由于树状数组只能维护单点修改,因此我们考虑将区间修改转换为单点修改,也就是差分操作,但是我们怎么通过一个差分数组去求前缀和呢?
这里需要进行一些数学推导,我们设差分数组为 \(d\),要求的前缀和为 \(p_i\),经过修改后的原数组为 \(a\)
那么我们可以知道 \(a_i=\sum_{j=1}^{i} d_j\),又因为 \(p_i=\sum_{j=1}^{i} a_j\) 所以我们可以得到 \(p_i=\sum_{j=1}^{i} \sum_{k=1}^{j} d_k\)
但是这样依然不好处理,所以我们对上面的等式进行变形:我们不再求出每个数之后求和,而是考虑求出差分数组中的每一个元素对 \(p_i\) 的贡献
那么 \(p_i =\sum_{j=1}^{i}(d_j*\sum_{k=j}^{i} 1)\)
也就是 \(p_i=\sum_{j=1}^i[d_j*(i-j+1)]\)
我们再将括号拆开 \(p_i=\sum_{j=1}^{i} d_j*(i+1)-\sum_{j=1}^{i}d_j*j\)
所以我们维护两个数组 \(a_i=d_i,b_i=d_i*i\) 即可
struct Binary_tree{
int a[N],b[N];
int lowbit(int x){
return x&-x;
}
void clear(){
memset(a,0,sizeof(a));
memset(b,0,sizeof(b));
}
void add(int x,int v){
for(int i=x;i<=n;i+=lowbit(i)){
a[i]+=v;
b[i]+=x*v;
}
}
void add(int l,int r,int v){
add(l,v),add(r+1,-v);
}
int query(int x){
int res=0;
for(int i=x;i;i-=lowbit(i)){
res+=a[i]*(x+1);
res-=b[i];
}
return res;
}
int query(int l,int r){
return query(r)-query(l-1);
}
}
6.权值树状数组
权值树状数组类似于一个计数数组,也就说,记录每一个值出现过多少次,这就可以以更优秀的时间复杂度解决许多问题
比如说求序列中第 \(k\) 小的值,我们可以使用一个类似于快速排序的方法来求出第 \(k\) 小,但是很显然这样子的做的时间复杂度为 \(O(n)\)
但是我们可以利用权值树状数组将这个时间复杂度优化到 \(O(logn)\)
这里运用了倍增的思想
int kth(int k) {
int x=0;
for (int i=log2(n);i>=0;i--){
if(x+(1<<i)<n&&a[x+(1<<i)]<k){
x+=(1<<i);
k-=a[x];
}
}
return x+1;
}

浙公网安备 33010602011771号