先放核心代码:
void updata(int x,int y)
{
while(x<=100000)
{
cnt[x]+=y;
x+=x&(-x);
}
}
int summ(int x)
{
int a=0;
while(x>0)
{
a+=cnt[x];
x-=x&(-x);
}
return a;
}
树状数组的基础是一个被构造出来的式子:C[i]=A[i]+A[i-1]+....+A[i-2^k+1];k代表i的二进制的最后连续0的个数 比如 对于1000和101000,k=3。至于这个式子是怎么被构造出来的,k为什么要代表这个。因为二进制的思想。
根据这个图来看节点与其子树的关系
接下来则很容易发现 节点和子节点的是有关系的,这种关系就是 i=j+lowbit(j); lowbit是j的最低位1所代表的数字 比如对于 1000(8的二进制) 1000=100+lowbit(100)=110+lowbit(110)=111+lowbit(111);
这个关系是树状数组的核心,有了这个关系,我们可以把子区间的变化以log2n的次数传递上去
那我们也知道了 当我们要求 1-n的和时,我们同样把n表示为2进制,我们知道 C[i]=A[i]+A[i-1]+....+A[i-2^k+1]; 所以对于i 是不是我们只要把他所有的1都用上 就可以表示1-n的和?
举个例子 求1-11000 则 Sum(11000)=C[11000]+C[10000]; 因为 根据C[i]的构造方法 C[i]是从A[i]开始的2^k个元素的和,则C[11000]求了A[11000] A[10111] A[10110] A[10101] A[10100] A[10010] A[10001] 这2^k个数 然后接着C[10000]求出了剩下 10000个元素的和 到了这我们就大概了解了树状数组的发明者的天才的构造是从何而来的了 普通的求1-n的和储存的数据太多,而这位天才则想,我们能不能根据二进制的思想来储存这些值呢?任何一个数,都可以由若干个二进制数相加而成,如果我们在求Sum(n)之前就知道了 n对应的二进制数从最低位开始,每个1所代表的数字的前2^i个数的和,我们不就能在时间复杂度log2(n)内求出所有的值 比如 101110 我们如果知道 101110->101101 101100->101001 101000->100001 100000->1各自的和,就可以在空间复杂度和时间复杂度很小的情况下求出1-101110了
然后对于区间和的修改 又利用每个小区间向上转移 修改了大区间的和
总的来说 树状数组就是利用了二进制的思想求和 写的有点乱 但只要你看懂了 二进制思想的那部分 相信看懂和实现树状数组并不难。
一、树状数组是干什么的?
平常我们会遇到一些对数组进行维护查询的操作,比较常见的如,修改某点的值、求某个区间的和,而这两种恰恰是树状数组的强项!当然,数据规模不大的时候,对于修改某点的值是非常容易的,复杂度是O(1),但是对于求一个区间的和就要扫一遍了,复杂度是O(N),如果实时的对数组进行M次修改或求和,最坏的情况下复杂度是O(M*N),当规模增大后这是划不来的!而树状数组干同样的事复杂度却是O(M*lgN),别小看这个lg,很大的数一lg就很小了,这个学过数学的都知道吧,不需要我说了。申明一下,看下面的文章一定不要急,只需要看懂每一步最后自然就懂了。
二、树状数组怎么干的?
先看两幅图(网上找的,如果雷同,不要大惊小怪~),下面的说明都是基于这两幅图的,左边的叫A图吧,右边的叫B图:
是不是很像一颗树?对,这就是为什么叫树状数组了~先看A图,a数组就是我们要维护和查询的数组,但是其实我们整个过程中根本用不到a数组,你可以把它当作一个摆设!c数组才是我们全程关心和操纵的重心。先由图来看看c数组的规则,其中c8 = c4+c6+c7+a8,c6 = c5+a6……先不必纠结怎么做到的,我们只要知道c数组的大致规则即可,很容易知道c8表示a1~a8的和,但是c6却是表示a5~a6的和,为什么会产生这样的区别的呢?或者说发明她的人为什么这样区别对待呢?答案是,这样会使操作更简单!看到这相信有些人就有些感觉了,为什么复杂度被lg了呢?可以看到,c8可以看作a1~a8的左半边和+右半边和,而其中左半边和是确定的c4,右半边其实也是同样的规则把a5~a8一分为二……继续下去都是一分为二直到不能分,可以看看B图。怎么样?是不是有点二分的味道了?对,说白了树状数组就是巧妙的利用了二分,她并不神秘,关键是她的巧妙!
她又是怎样做到不断的一分为二呢?说这个之前我先说个叫lowbit的东西,lowbit(k)就是把k的二进制的高位1全部清空,只留下最低位的1,比如10的二进制是1010,则lowbit(k)=lowbit(1010)=0010(2进制),介于这个lowbit在下面会经常用到,这里给一个非常方便的实现方式,比较普遍的方法lowbit(k)=k&-k,这是位运算,我们知道一个数加一个负号是把这个数的二进制取反+1,如-10的二进制就是-1010=0101+1=0110,然后用1010&0110,答案就是0010了!明白了求解lowbit的方法就可以了,继续下面。介于下面讨论十进制已经没有意义(这个世界本来就是二进制的,人非要主观的构建一个十进制),下面所有的数没有特别说明都当作二进制。
上面那么多文字说lowbit,还没说它的用处呢,它就是为了联系a数组和c数组的!ck表示从ak开始往左连续求lowbit(k)个数的和,比如c[0110]=a[0110]+a[0101],就是从110开始计算了0010个数的和,因为lowbit(0110)=0010,可以看到其实只有低位的1起作用,因为很显然可以写出c[0010]=a[0010]+a[0001],这就为什么我们任何数都只关心它的lowbit,因为高位不起作用(基于我们的二分规则它必须如此!),除非除了高位其余位都是0,这时本身就是lowbit。
既然关系建立好了,看看如何实现a某一个位置数据跟改的,她不会直接改的(开始就说了,a根本不存在),她每次改其实都要维护c数组应有的性质,因为后面求和要用到。而维护也很简单,比如更改了a[0011],我们接着要修改c[0011],c[0100],c[1000],这是很容易从图上看出来的,但是你可能会问,他们之间有申明必然联系吗?每次求解总不能总要拿图来看吧?其实从0011——>0100——>1000的变化都是进行“去尾”操作,又是自己造的词--'',我来解释下,就是把尾部应该去掉的1都去掉转而换到更高位的1,记住每次变换都要有一个高位的1产生,所以0100是不能变换到0101的,因为没有新的高位1产生,这个变换过程恰好是可以借助我们的lowbit进行的,k +=lowbit(k)。
好吧,现在更新的次序都有了,可能又会产生新的疑问了:为什么它非要是这种关系啊?这就要追究到之前我们说c8可以看作a1~a8的左半边和+右半边和……的内容了,为什么c[0011]会影响到c[0100]而不会影响到c[0101],这就是之前说的c[0100]的求解实际上是这样分段的区间 c[0001]~c[0001] 和区间c[0011]~c[0011]的和,数字太小,可能这样不太理解,在比如c[0100]会影响c[1000],为什么呢?因为c[1000]可以看作0001~0100的和加上0101~1000的和,但是0101位置的数变化并会直接作用于c[1000],因为它的尾部1不能一下在跳两级在产生两次高位1,是通过c[0110]间接影响的,但是,c[0100]却可以跳一级产生一次高位1。
可能上面说的你比较绕了,那么此时你只需注意:c的构成性质(其实是分组性质)决定了c[0011]只会直接影响c[0100],而c[0100]只会直接影响[1000],而下表之间的关系恰好是也必须是k +=lowbit(k)。此时我们就是写出跟新维护树的代码:
有了上面的基础,说求和就比较简单了。比如求0001~0110的和就直接c[0100]+c[0110],分析方法与上面的恰好逆过来,而且写法也是逆过来的,具体就不累述了:
三、总结一下吧
首先,明白树状数组所白了是按照二分对数组进行分组;维护和查询都是O(lgn)的复杂度,复杂度取决于最坏的情况,也是O(lgn);lowbit这里只是一个技巧,关键在于明白c数组的构成规律;分析的过程二进制一定要深入人心,当作心目中的十进制。
对于普通数组,其修改的时间复杂度位O(1),而求数组中某一段的数值和的时间复杂度为O(n),因此对于n的值过大的情况,普通数组的时间复杂度我们是接受不了的。
在此,我们引入了树状数组的数据结构,它能在O(logn)内对数组的值进行修改和查询某一段数值的和。
树状数组是一个查询和修改复杂度都为log(n)的数据结构,假设数组a[1..n],那么查询a[1]+...+a[n]的时间是log级别的,而且是一个在线的数据结构,支持随时修改某个元素的值,复杂度也为log级别。
假设A[]数组为存储原来的值得数组,C[]为树状数组。
我们定义:C[i] = A[i - 2^k + 1] + ..... + A[i] 其中k为i用二进制表示时的末尾0的个数。例如:i= 10100,则k = 2,i = 11000,则k = 3;为了方便我直接写的是二进制的i,请读者注意。
此时我们可以知道,C[i] 它里面包含了2^k个A[]元素,这2^k个元素是从C[i]往后一直递减的2^k个元素,即i 一直减小的。
其中我们有一种快速的求解2^k的值得方法:
利用机器的补码原理也可以写成这样:
下面我们还要求的就是如何快速的修改某一个元素的值以及求出某一段元素值的和;
先来看它是怎么快速修改某一个元素的值的:
我们举个例子(为了方便i的值直接写成二进制了):i = 11000,此时k = 3;
这2^k = 3 个数即为:A[11000],A[10111],A[10110],A[10101],A[10100],A[10011],A[10010],A[10001]
C[11000] = A[11000]+A[10111]+A[10110]+A[10101]+A[10100]+A[10011]+A[10010]+A[10001];
这里我们会发现:
A[10100] + A[10011] + A[10010] + A[10001] = C[10100]
而
A[10010] + A[10001] = C[10010]
A[10011] = C[10011]
所以
C[10100] = C[10010] + C[10011] + A[10100]
又
A[10110] + A[10101] = C[10110]
而
A[10101] = C[10101]
所以
C[10110] = C[10101] + A[10110]
又
A[10111] = C[10111]
至此我们可以得出:
C[11000] = C[10100] + C[10110] + C[10111] + A[11000]
到这里我们可以得出:
k的值就表示子树的个数,子树即为树状数组的元素。
如图例:
这个个数就是可以从左往右一直加1,怎么加呢?只要保持这k个位左边都是1,右边都是0,且左边至少有一个1,右边可以没有0,而k那个位,原来是1,在加的过程中,就要变成0.如上例:k个位就是11000的末尾3个0,000;就这3个位而言,左边至少一个1,右边可以没有0,就有这三种:100,110,111.再就是把原来第k个位的1变成0,这样就可以得出它的三个子树为:10100,10110,10111.
这样得出的个数就是子树的个数,也等于k的值。
也许有读者会有疑问了,为什么就一定是这样呢?
那是因为,根据树状数组的定义,我们再根据二进制的加法进位的原则,可以得出任何数的树状数组值都可以用上述分解的子树的树状数组元素的值累加得到。(可以根据之前的例子进行理解,务必理解清楚)
有了上述的理解基础了,那么我们就可以知道如何快速的根据第i个节点求出它的父节点了,求法如下:
i的父节点为p,则p = i + lowbit(i);
根据前面的知识,这一点毋庸置疑。
现在我们再来理解一个知识:
k 表示的 为i 这个节点表示的树的深度。
为什么呢?
我们知道k为i的末尾0的个数,而小于i的节点的值肯定要小于i,那么它们的k绝对要小于i的k,而最长的就是k,因为它的二进制表示的数只能允许它右移k位,右移k位之后它就是叶子节点了,就只表示一个单一的A[]数组的值了,同时也是C[]树状数组的值。
有了这点知识为基础,那么我们就可以知道,我们要修改某个元素的值,就会修改C[]的值,以及它的所有祖先节点的值。
而我们已经知道,它的父节点的节点编号就是i + lowbit[i],一步就可以返回过去,而这个树的深度只有logn,所以我们往上一步一步的修改它的祖先节点就行了,且最多只要logn步,因此时间复杂度是O(logn)。实现函数代码如下:
快速修改已经实现了,那么接下来就要快速求出某一段元素的值的和了:
我们先来快速求出前n项元素的和,只要能快速求出前n项元素的和,那么快速求某一段元素的和就只要做一次减法就OK了。
那么前n项元素的和怎么快速的求出来呢?
我们来看看i = 11000(2进制),这个数,如何快速求出前i个元素的和。
我们知道:11000 = 2^3 + 2^4
也就是它总共有2^3+2^4 个元素,我们回想一下前面的知识,C[11000]包括了几个元素,2^3个?对!,就是2^k,k=lowbit(11000) 个,而这2^3个就是11000一直递减的2^3个,而还有2^4个呢?聪明的读者会发现那是不是就是C[10000]包括的元素的个数呢?就是2^k,k=lowbit(10000).而这2^4个刚好是接着11000的2^3个之后的2^4个。
所以S[11000] = C[11000] + C[10000],S[i]为前i项的和。
现在我们来想想为什么就是这么刚好呢?
我们根据二进制的组成,可以得出,任何一个数i都可以表示成2的幂次方的和的一个组成形式,而树状数组C[i]表示的是2^k,k=lowbit(i)个元素相加的和,这些元素是从第i个元素往下数i一直递减而得到的。那么我们是否可以用C[]的一些元素相加来求得S[]呢?
答案是可以的,刚好可以,k的值表示的是从这个数开始往下一直数掉2^k个,这2^k个数的值的和存在C[i]里,而数掉的这2^k个只会影响到第k位上的那个1的变动,变成了0。当数掉了2^k个数之后,
此时如果再往下数,会产生什么样的影响呢?会影响到k+1位的1的变动,变成0.我们先想一下,我们数掉2^k个数之后,得到的数不就是一个末尾是k+1个0个数了么!
因为2^k个数数完了,且在只有k个0的情况下,数完2^k个数之后末尾又会全是0,而第k位的1也会变成了0,所以此时这个数的末尾0的个数就是k+1,它可以往下数2^(k+1)个数,而这2^(k+1)个数的值的和刚好存在C[i - lowbit(i)]里。到这里我想聪明的读者肯定已经知道如何快速的求出前n项元素的和了。
我们只要一直往下数,数到左边没有1可以变动的时候,即我们从i数到了0,那么这i个数你就数完了,答案也就出来了。
在这个过程中,我们可以知道,我们每次要变动某个1的时候,我们都知道从它开始往下数的2^k,k=lowbit(i)个数的值的和,这个个总和存在了C[i]里,而每一个数n它最多能变动的1的个数只有logn个,所以我们在计算前n个元素的和的时候最多只要计算logn次的加法,因此求前n个元素的和的时间复杂度即为O(logn)。
下面给出函数实现代码:
至此,我们实现了在O(logn)的时间内,修改数组的元素和求得某一段元素的和了
问题:求一个数组中连续n项的和。
首先想到的肯定是做一个循环,把这个连续的n项加起来,时间复杂度为O(n)。复杂度为n,看起来还不错,再说了求n个数的和,怎么也要加n次吧,所以说这应该就是最优解了,但是一提交结果是Time Limit Exceeded,顿时傻眼了,难道还有复杂度更低的方法?
会不会有O(logn)的解法?
O(n)的那个算法,如果只操作一次还是可以接受的,但是如果需要大量的求和操作,比如第一次求下标(1,1234)的和第二次求下标(2,1024)的和,很容易发现在第一次计算的过程中(2,1024)的和是计算过的,只是没有保存下来,导致第二次求和的时候还要再算一遍。你有没有想过,如果事先把一部分的和先计算并保存起来,这样会不会更快一些呢?
Binary Indexed Tree(BIT)
其实树状数组(Binary Indexed Tree(BIT), Fenwick Tree)就是这样做的,他是一个查询和修改复杂度都为log(n)的数据结构。主要用于查询任意两位之间的所有元素之和,但是每次只能修改一个元素的值。
核心思想:
- 树状数组中的每个元素是原数组中一个或者多个连续元素的和。
- 在进行连续求和操作a[1]+…+a[n]时,只需要将树状数组中某几个元素的和即可。时间复杂度为O(lgn)
下面是一个示意图
a[]: 保存原始数据的数组
e[]: 树状数组,其中的任意一个元素e[i]可能是一个或者多个a数组中元素的和。如e[2]=a[1]+a[2]; e[3]=a[3],e[4]=a[1]+a[2]+a[3]+a[4]。
e[i]中的元素:如果数字 i 的二进制表示中末尾有k个连续的0,则e[i]是a数组中2^k个元素的和,则e[i]=a[i-2^k+1]+a[i-2^k+2]+…+a[i-1]+a[i]。也就是说,e[i]中每一个元素管理着a[]中若干个元素的和,并且各个元素管理的区间没有重叠。
如:4=100(2) e[4]=a[1]+a[2]+a[3]+a[4];
6=110(2) e[6]=a[5]+a[6]
7=111(2) e[7]=a[7]
计算2^k的两个方法
- 2^k = (i & (-i)); (利用机器补码特性)
- 2^k = (i & (i^(i-1));
父节点,子节点
父节点
是离它最近的,且编号末位连续0比它多的就是它的父亲,如e[2]是e[1]的儿子;e[4]是e[2]的儿子。
e[4] = e[2]+e[3]+a[4] = a[1]+a[2]+a[3]+a[4] ,e[2]、e[3]的后继就是e[4]。
计算方法
lowbit(i) = ( (i-1) ^ i) & i ; //或者(i & (-i))
节点e[i]的父节点为 e[ i - lowbit(i) ]
子节点
最近的,编号即为比自己小的,最末连续0比自己多的节点。如e[7]的子节点是e[6],e[6]的子节点是e[4]
计算方法
lowbit(i) = ( (i-1) ^ i) & i ; //或者(i & (-i))
节点e[i]的子节点为 e[ i + lowbit(i) ]
实现代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
树状数组(Binary Indexed Tree) 总结
1.“树状数组”数据结构的一种应用
对含有n个元素的数组(a[1],...,a[k],...,a[n]):
(1)求出第i个到第j个元素的和,sum=a[i]+...+a[j]。
进行j-i+1次加法,复杂度为O(j-i+1)
(2)任意修改其中某个元素的值。
使用数组下标可以直接定位修改,时间复杂度为O(1)
对于同时支持上述两种操作的系统中,求和操作(1)求任意连续个数组元素和的平均时间复杂度为O(n),修改操作(2)时间复杂度是O(1)。如果系统中大量进行上述两种操作m次,其中执行操作(1)概率1/p,操作(2)概率1-1/p,则系统时间复杂度为:
可以使用树状数组使得上述两种操作的时间复杂度为O(m*logn)。
2.树状数组介绍
核心思想:
(1)树状数组中的每个元素是原数组中一个或者多个连续元素的和。
(2)在进行连续求和操作a[1]+...+a[n]时,只需要将树状数组中某几个元素的和即可。时间复杂度为O(lgn)
(3)在进行修改某个元素a[i]时,只需要修改树状数组中某几个元素的和即可。时间复杂度为O(lgn)
下图就是一个树状数组的示意图:
解释如下:
1) a[]: 保存原始数据的数组。(操作(1)求其中连续多个数的和,操作(2)任意修改其中一个元素)
e[]: 树状数组,其中的任意一个元素e[i]可能是一个或者多个a数组中元素的和。如e[2]=a[1]+a[2]; e[3]=a[3]; e[4]=a[1]+a[2]+a[3]+a[4]。
2) e[i]是几个a数组中的元素的和?
如果数字 i 的二进制表示中末尾有k个连续的0,则e[i]是a数组中2^k个元素的和,则e[i]=a[i-2^k+1]+a[i-2^k+2]+...+a[i-1]+a[i]。
如:4=100(2) e[4]=a[1]+a[2]+a[3]+a[4];
6=110(2) e[6]=a[5]+a[6]
7=111(2) e[7]=a[7]
3) 后继:可以理解为节点的父亲节点。是离它最近的,且编号末位连续0比它多的就是它的父亲,如e[2]是e[1]的后继;e[4]是e[2]的后继。
如e[4] = e[2]+e[3]+a[4] = a[1]+a[2]+a[3]+a[4] ,e[2]、e[3]的后继就是e[4]。
后继主要是用来计算e数组,将当前已经计算出的e[i]添加到他们后继中。
前驱:节点前驱的编号即为比自己小的,最近的,最末连续0比自己多的节点。如e[7]的前驱是e[6],e[6]的前驱是e[4]。
前驱主要是在计算连续和时,避免重复添加元素。
如:Sum(7)=a[1]+...+a[7]=e[7]+e[6]+e[4]。(e[7]的前驱是e[6], e[6]的前驱是e[4])
计算前驱与后继:
lowbit(i) = ( (i-1) ^ i) & i ;
节点e[i]的前驱为 e[ i - lowbit(i) ];
节点e[i]的前驱为 e[ i + lowbit(i) ]
3.树状数组代码示例

1 #include <iostream> 2 #include <stdio.h> 3 4 using namespace std; 5 6 int input(int*,int*,int); ///输入数据 7 int calStageSum(int*,int); ///计算树状数组 8 int getSum(int*,int); ///求出前n个数字的和 9 int updataElement(int*,int*,int,int,int); ///更新某一位置上的元素 10 11 int main (){ 12 int n; 13 int newValue; 14 cout<<"Input the n(n>3) :"; 15 cin>>n; 16 17 int *num = new int[n+1]; 18 int *sum = new int[n+1]; 19 20 cout<<"Input "<<n<<" numbers"<<endl; 21 input(num,sum,n); 22 calStageSum(sum,n); 23 24 cout<<"The sum of first three number:"<<getSum(sum,3)<<endl; 25 26 cout<<"Update the 2nd number value:"; 27 cin>>newValue; 28 updataElement(sum,num,n,2,newValue); 29 30 cout<<"The sum of first three number:"<<getSum(sum,3)<<endl; 31 32 delete []num; 33 delete []sum; 34 return 0; 35 } 36 37 int input(int* num,int *sum,int n){ 38 for(int i=1;i<=n;i++){ 39 cin>>num[i]; 40 sum[i] = num[i]; 41 } 42 return 0; 43 } 44 45 int calStageSum(int *sum,int n){ 46 int lowbit; 47 int par; 48 for(int i=1;i<=n;i++){ 49 lowbit = ((i-1)^i)&i; 50 par = lowbit+i; ///后继节点id 51 if(par <= n){ 52 sum[par] = sum[par] + sum[i]; 53 } 54 } 55 return 0; 56 } 57 58 int getSum(int* sum,int n){ 59 int sumPreN = 0; 60 int lowbit = 0; 61 while(n!=0){ 62 sumPreN += sum[n]; 63 lowbit = ((n-1)^n)&n; 64 n = n - lowbit; ///前驱节点id 65 } 66 return sumPreN; 67 } 68 69 int updataElement(int* sum,int *num,int n,int pos,int newvalue){ 70 int lowbit = 0; 71 int dis = newvalue - num[pos]; 72 num[pos] = newvalue; 73 sum[pos] = sum[pos]+dis; 74 75 while(true){ 76 lowbit = ((pos-1)^pos)&pos; 77 pos = pos + lowbit; ///后继节点id 78 if(pos <= n){ 79 sum[pos] = sum[pos]+dis; 80 } 81 else 82 break; 83 } 84 return 0; 85 }