树状数组看了很久,因为看到的文章几乎都是告诉我是什么,而没有告诉我为什么。这篇文章集合我看过的所有资料加上我自己的思考。
先讲知识点再讲理论逻辑吧,不然脑袋里没图,云里雾里。
先来讲个函数:lowbit(n)
它的作用就是获得非负整数n在二进制表示下最低位1及其后面的0构成的数值
如,二进制为101100的44,lowbit(44)就获得二进制数100,也就是十进制的4
lowbit(n)=n&(~n+1)=n&(-n)
然后,看两个操作:
①将数组中的第x个数加上k
②输出区间[x,y]内每个数的和
第一个贼简单,直接加就是对吧。那第二个一看就是要用前缀和。也没什么难的嘛。but是这两个操作都要实现。
当你不搞前缀和,就可以第一个操作直接加。但是第二个操作的时间复杂度就很高。
当你搞了前缀和,第二个操作就很快。但是第一个操作就很麻烦。每改变一个数,这个数之后的所有数都要更新一遍。
所以现在要避免某一个操作效率差的情况。
首先原来的数组是一定不行的。肯定要对数组做点什么。可是又不能用前缀和。
那我们来想想。为什么前缀和数组效率差呢,还不是因为它们的关联性太强了。每改变一个数,这个数之后的所有数都要更新一遍。简直是牵一发而动全身。
回想一下数据结构,最简单的链表和数组,关联性都很强。但是树不一样,树的一个节点改变,最多影响它的父节点。不影响其它节点。
树的结构再加上二进制就是我们讲的树状数组
上一张二进制的图,这张图是b站上鹤翔万里up主的视频截图
可以看到,对于位置i,这棵树的第一层结点为全体
即所有lowbit(i)=1
第二层结点为全体
即所有的lowbit(i)=2
(以上黑体字是知乎上SleepyBag大佬的解释,我觉得他讲得非常清晰明了)
再来看遍图
那么这棵树的两个特点也就出来了。第一个就是上面的黑体字,另一个特点就是每个节点保存它所有的子节点的和。
因为比如1010(二进制)加上它的lowbit=2,也就是加上二进制的10(1010的最低位)
1010+10=11000,它的lowbit就升了一级。所以
t[x]节点的父节点为t[x+lowbit(x)]
根据图可知,每个节点t[x]的长度等于lowbit(x),所以每个t[x]保存的是它的下标x减去它的长度再加一开始的数组元素一直到a[x],配合图再理解下面的公式。
现在说说树状数组的基本运用
①修改某一点的值,add(x,k)操作
因为一个节点更改,它只影响它的父节点们。那就一层一层往上改呗。
这是鹤翔千里前辈的视频截图
代码如下:
void add(int x,int k){
for(;x<=n;x+=lowbit(x) t[x]+=k;
}
②询问第x个元素的前缀和,ask(x)操作
看图我们可以知道,要求x点的前缀和,就需要往一层一层加左上的点。
第一层的点就是因为它的lowbit比第二层的lowbit小,也就是第一层比第二层多了低位的1。那么把这个低位的1减去,就得到了上一层的点。
比如7(0111)就是比6(0110)多了个最低位的1,把这个最低位1减去就是6了。
两层之间的差异自然只有一个lowbit,所以往左上找上一个节点,只需要将下标-=lowbit(这个节点的下标)
代码如下:
int ask(int x){
int ans=0;
for(;x;x-=lowbit(x))ans+=t[x];
return ans;
}
另外,如果哦需要求区间和,我们可以分别求出前缀和并相减
要求某一个点的值可以ask(k)-ask(k-1)
树状数组主要是用于动态维护前缀和。比起算法,它更像一种工具。
树状数组的知识理论就讲到这里吧,这篇文章的图片和代码都是B站上鹤翔万里前辈的,大家也可以去找找他的视频看看,讲得非常好。我的文章主要是从原理和细节处做了很多自己的理解和思考。下篇文章讲一下树状数组的应用吧,小朋友排队的那道题。过几天应该会写吧…orz