简介
树状数组这个东西。。。有人说它像线段树。。。
其实感觉两者并没有什么直接联系。。。但是都是类似于树形操作的思想,所以经常把他俩放在一块说。。。
稍微讲讲我对这个算法的见解,大家看看对不对。。。
基本思想
基本思想是这样的:
首先来讲,树状数组较好的利用了二进制。它的每个节点的值代表的是自己和前面一些元素的和。至于到底是前面哪些元素,这就由这个节点的下标决定。
比如下面这个图(图中的数字代表节点的下标):
我们假设a[MAXN]数组用来存储初始数据,e[MAXN]代表了树状数组存储内容。
例如在上图的树状数组中,e[8]号记录了a[1]…a[8]的和,即e[4]+e[6]+e[7]+a[8]。绿色的线代表树的节点从哪些节点求和得到。
所以根据以上性质,树状数组实现的功能有:
将一个数组转化成树状数组
改变某一个点的值
询问a[1]+a[2]+······+a[x]的值
具体原理
具体的就是二进制的原理,比较绕。。。先从数据结构开始讲。。。
数据结构
看图,这个图是上面那个图的所有树状数组节点编号变成二进制以后的样子:
你有木有发现什么蹊跷之处?(并木有发现)
树状数组的节点深度其实就是它的节点编号的二进制形式中,从右往左数第一个1出现的位置。(这谁能发现啊喂!)
比如说:
6的二进制形式中(110),从右往左数第一个1出现的位置是2
7的二进制形式中(111),从右往左数第一个1出现的位置是1
8的二进制形式中(1000),从右往左数第一个1出现的位置是4
我们定义寻找这个“数字的二进制形式中,从右往左数第一个1出现的位置”(啊啊啊这个名字好长啊啊啊啊)的函数为lowbit(x)lowbit(x)函数。
即:
lowbit(6)=2lowbit(6)=2
lowbit(7)=1lowbit(7)=1
lowbit(8)=4lowbit(8)=4
然后你还会发现,一个节点并不一定是代表自己前面所有元素的和。只有满足2n2n这样的数才代表自己前面所有元素的和。
那么归纳一下,就能得出结论:
设节点的编号为xx,那么这个的值等于 a[x−2lowbit(x)−1+1]a[x−2lowbit(x)−1+1] 到 a[x]a[x] 的和。
比如说e[6]=a[6−22−1+1]+a[6]=a[5]+a[6]e[6]=a[6−22−1+1]+a[6]=a[5]+a[6]
再比如说e[8]=a[8−24−1+1]+a[8−24−1+2]+⋅⋅⋅+a[8]=e[4]+e[6]+e[7]+a[8]e[8]=a[8−24−1+1]+a[8−24−1+2]+···+a[8]=e[4]+e[6]+e[7]+a[8]
这就是树状数组比较神奇的地方之一。
lowbit函数的实现
这个地方用到的姿势是我难以去证明的。。。
但是我只能告诉你:
lowbit(x)=(−x)lowbit(x)=(−x)&xx
在这里要注意:
‘&’符号是按位取“与”运算。比如:10111011&0110=00100110=0010
负数进行按位取“与”运算的时候,是对负数的“补码”进行的。
什么是补码?以下选自百度百科
计算机中的符号数有三种表示方法,即原码、反码和补码。三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位,三种表示方法各不相同。
补码具有的性质:
1、对一个整数的补码再求补码,等于该整数自身。
2、补码的正零与负零表示方法相同。
求给定数值的补码分以下两种情况:
正数
正整数的补码是其二进制表示,与原码相同 。
【例】+9的补码是00001001。
(备注:这个+9的补码是用8位2进制来表示的,补码表示方式很多,还有16位二进制补码表示形式,以及32位二进制补码表示形式,64位进制补码表示形式等。每一种补码表示形式都只能表示有限的数字。)
负数
求负整数的补码,将其对应正数二进制表示所有位取反(包括符号位,0变1,1变0)后加1。
同一个数字在不同的补码表示形式中是不同的。比如-15的补码,在8位二进制中是11110001,然而在16位二进制补码表示中,就是1111111111110001。以下都使用8位2进制来表示。
【例】求-5的补码。
-5对应正数5(00000101)→所有位取反(11111010)→加1(11111011)
所以-5的补码是11111011。
然后嘛,补码,按位与运算。
啊啊啊我也是真的不知道为什么会这么神奇啊。。。反正这样一计算就得到“一个数的二进制形式中,从右往左数第一个1出现的位置”了。。。
比如:
lowbit(6)=00000110lowbit(6)=00000110&11111010=00000010=211111010=00000010=2
真是十分有趣。。。脑子是个好东西,真希望我有。。。
修改一个元素的值
因为树状数组的特殊性质,我们只需要修改所有包含这个元素的节点就行了。
那怎么操作呢?
假设本次对e[x]e[x]进行了操作,那么下一次就对e[x+lowbit(x)]e[x+lowbit(x)]进行操作就行了。
至少看图是这样没错。论证。。。(此处省略101010101010字论证过程,其实我不会233333)
好吧。。。没有论证。。。CPP代码上来!
void add(int x,int v)
{
while(x<=len)
{
e[x]+=v;
x+=lowbit(x);
}
}
查询
查询是为了得到a[1]+a[2]+⋅⋅⋅⋅⋅⋅+a[x]a[1]+a[2]+······+a[x]的值。
同样是看图。。。同样是因为一些非常特殊的性质。。。
所以。。。讲一下怎么进行一波操作。。。我的能力无法证明它。。。
对于目标xx。。。
每一次操作:
答案加上xx对应节点的值,然后将xx减去它的lowbitlowbit,继续进行这样的操作,直到xx小于等于00。
不信。。。用图片验证一下吧。。。我也证明不了了。
int query(int x)
{
int sum=0;
while(x>0)
{
sum+=e[x];
x-=lowbit(x);
}
return sum;
}