树状数组 从入门到不会(超详细)

树状数组

这是一种基于二进制思想的数据结构,他的主要功能是维护前缀和,支持修改与求和两种操作(复杂度都是O(nlogn)),这其中又分为单点修改,区间修改,区间查询,本文会依次介绍。

入门

对于一个数,我们可以用他的二进制表示来把他拆成几个小区间,以(10101)为例,它可以拆成 $2^4 + 2^2 + 2^0$ 所以就可以得到三个区间分别为 $ \left [ 1,2^4 \right ] \left [ 2^4+1,2^4+2^2 \right ] \left [ 2^4+2^2+1,2^4+2^2+2^0 \right ]$

进而我们可以知道,对于查询一个结尾为 $i$ 的区间,得到的区间长度就是二进制下他最小的2的幂,定义为 $lowbit(i)$ 要想求出他也很简单,就是 $i$ & $-i$ ,因为计算机中负数使用的是补码表示法,也就是按位取反再加一,这会使原来的0变成1,1变成0,然后加一又会使原来低位的0重新变回0,直到原来最低位的1重新变回1,故而得到了$lowbit$ 不理解可以手推一遍。

到此,我们可以建立一个树状数组,$c[i]$ 存储了给定序列$A$中 $\left [ i-lowbit(i)+1,i \right ] $区间的元素和,也就是以$i$ 为根节点的子树的叶子结点的和,也不难发现,该节点的父亲节点为 $c[i+lowbit(i)]$ ,他有 $lowbit(i)$个叶子节点。

附上我们构造出的数组结构图片,就能理解为什么叫树状数组了

 百度yyds 

单点修改

这颗数的深度是$log$的,因此对每个元素的修改操作,在树状数组中只需要 $log$的修改,是不是很棒呢。

我们只需要不断寻找当前节点的父亲节点,也就是加上$lowbit(i)$ 并给他们也加上这个元素,就可以啦

inline void update(int i,int x){
    for(;i<=n;i+=lowbit(i))c[i]+=x;
}

 

$c$是我们的树状数组,$i$是原序列修改的点,$x$是加上的数。

查询区间前缀和

我们可以发现,对于当前的$c[i]$ 他求出了 $\left [ i-lowbit(i)+1,i \right ] $区间的元素和,那么我们只需要每次减掉$lowbit(i)$就可以得到下一个大区间的和,累加即可得到前缀和,复杂度显然也是$O(logn)$的

inline int sum(int i){
    int ans = 0;
    for(;i;i-=lowbit(i))ans+=c[i];
    return ans;
}

完结撒花~~~

恭喜你,看到这里已经成功入门了树状数组。

接下来我们介绍对于他的升级:

区间修改:对给定区间同时加减一个数k & 单点查询:查询修改后给定位置元素的值

乍一看是不是感觉会非常非常非常非常非常非常麻烦。

不过我们有个好东西,叫差分数组,如果还不了解的话请自行百度。

众所周知,差分与前缀和是互逆操作,说成人话就是:差分的前缀和就是原数列$A$,前缀和的差分也是原数列$A$。

看到这里是不是有点眉目了?

是的,在这里我们的差分数组不再是原数列$A$的前缀和,我们可以先引入一个数组$d$,$A{}'[i] = d[i] + A[i]$ 也就是说,$d[i]$记录了$A[i]$被修改的值。

显然, $d$的初始值为0。

那么考虑$d$的差分数组,记为$B$ ,那么我们只需要求出$B$的前缀和再加上$A[i]$,就能得到$A{}'[i]$,也就是修改后的值。

这时我们的树状数组c维护的就是$B$的前缀和了,修改区间$\left [ x,y \right ]$的操作只需要$update(x,k) update(y+1,-k)$ 即可

查询修改后的元素值就是 $sum(i) + A[i]$ 

 

究极进化 : 区间修改后的区间查询

这次我们考虑对原数组$A$差分, 记为 $a$ 吧 ,那么树状数组维护的就是$a$的前缀和,也就是说 $A_{n^{}} = \sum_{i=1^{}}^{n}c_{i}$

修改操作也是和原来的一样,$update(x,k) update(y,-k) $即可 那么如何查询区间和呢?

不难发现有如下表达式$\sum_{i=1}^{n}A_{i} = \sum_{i=1}^{n}\sum_{j=1}^{i}c_{j}$

观察后发现,$c[1]$出现了$n$次,$c[k]$出现了$(n-k+1)$次

所以

$\sum_{i=1}^{n}A_{i} = \sum_{i=1}^{n}(c_{i}*(n+1)-c_{i}*i)$

所以我们只需要维护两个树状数组,一个是$c[i]$ 一个是$c[i]*i$

inline void update(int i,int x){
    for(;i<=n;i+=lowbit(i))f[i]+=x;
}//更新f[i] -> c[i]
inline int sum(int *tree,int x){
    int ans = 0;
    for(;x;x-=lowbit(x))ans+=tree[x];
    return ans;
}//统一求和
inline void update1(int i,int x){
    int j = i;
    for(;i<=n;i+=lowbit(i))c[i]+=x*j; 
}//更新c[i] -> c[i] * i
int s1 = (y+1)*sum(f,y) - sum(c,y);
int s2 = x*sum(f,x-1) - sum(c,x-1);
printf("%d\n",s1-s2);
//求修改后区间和

 

看完留个赞呗 逃

 

posted @ 2022-02-15 18:51  Xu_brezza  阅读(58)  评论(0编辑  收藏  举报