一.概念程序员
树状数组(Binary Indexed Tree(B.I.T), Fenwick Tree)是一个查询和修改复杂度都为log(n)的数据结构。主要用于查询任意两位之间的全部元素之和,可是每次只能修改一个元素的值;通过简单修改能够在log(n)的复杂度下进行范围修改,可是这时只能查询其中一个元素的值(若是加入多个辅助数组则能够实现区间修改与区间查询)。算法
要想查看本题,请点这里(有一些区别,本质相同)数组
若是直接用数组进行模拟,修改的时间复杂度是O(1),查询是O(n),m次查询操做的时间复杂度就是O(mn),时间复杂度太高。数据结构
树状数组就能够轻松处理这类问题,它是一个查询和修改的复杂度都为log(n)的数据结构。函数
A表明原来的数组,C表明树状数组。为何树状数组要长成这样?优化
明确一点:树状数组是对二进制的应用。spa
咱们不妨把全部的数字都转换为二进制。来观察一下数字特征:code
举几个例子:blog
用C来表明树状数组,用A来表明原数组:图片
C(2) = A(1)+ A(2) 对应二进制 C(0010) = A(0010) + A(0001)
C(4) = A(4)+A(2)+A(3) 对应二进制C(0100) = A(0100)+A(0010)+ A(0011)
C(8) = A(8)+A(4)+A(6)+A(7)对应二进制C(1000) = A(1000)+A(0100)+A(0110)+A(0111)
......
不难发现,对于任意的C[i]写成二进制的形式,都等于原来的数组A[i]的值自己,加上把i转换成二进制后,把首位变成0,而且开始逐位向后把0变成1,相加。
例如: 1000(二进制)向后逐位变化,即1000--> 0100-->0110>0111
......
构建出这样的树状数组后:
查询A1到A8的和,只须要返回C8
查询A1到A7,只需返回C7+C6+C4
查询A1到A6,只须要返回C6+C4
以此类推。
这样便优化了询问的时间复杂度。
那么说的这么好听,如何作到修改A的值时,顺便更新C的全部关于A的值呢?(例如,修改A[1]的时候更新C1,C2,C4,C8)
三.补码、lowbit函数
1.补码
首先说一说补码。
——百度百科。
补码是计算机表示符号数的一种方式,概念内容太杂乱,对于咱们树状数组没有太大的用处,只须要了解两个事情:
·正整数的补码和原码(原来的二进制的代码)相同。
·负整数的补码将其正整数的原码全部为取反(0变1,1变0),以后加一。
2.lowbit函数
lowbit函数即是帮助咱们找到二进制表达式中最低位1所对应的值。
好比,6的二进制是110,因此lowbit(6)=2。
lowbit的实现方式一共两种:我的推荐下面这种写法:
int lowbit(int x) { return x&(-x); }
&符号意思为按位与,把两个数的二进制一位一位比较,都为1,结果则为1,不然就是0。
这个函数什么意思?将一个数x的原码和补码按位与?返回的就是最后一位1?
的确是这样。
举个例子(example):
6 = 0110(二进制)
它的补码为0010。按位与以后获得的确实就是最后一位1。
正是由于补码在取反后+1,才有了lowbit函数的产生。
您不妨打开计算器,切换到程序员模式,试上几组,或许会有新的领悟。
毕竟“纸上得来终觉浅,绝知此事要躬行”。
......
3.lowbit查询与更新的应用:
首先先来运行一段代码:
#include<stdio.h> int main() { int i,j; for(i=1;i<=8;i++) { printf("%d:",i); for(j=i;j<=8;j+=j&-j) printf("%d ",j); printf("\n"); } return 0; }
Output:
1: 1 2 4 8
2: 2 4 8
3: 3 4 8
4: 4 8
5: 5 6 8
6: 6 8
7: 7 8
8: 8
图片数字对比来看更新A1,须要更新C1,C2,C4,C8。
更新A2,须要更新C2,C4,C8。更新A3,须要更新C3,C4,C8.....
正好与咱们代码运行出来的结果一致。这就为咱们向上更新提供了条件。
再来看一下查询。若是查询A1到A8和,只须要返回C8,查询A1到A7,只须要返回C7+C6+C4
再来看下面这组代码:
#include<stdio.h> int main() { int i,j; for(i=1;i<=8;i++) { printf("%d:",i); for(j=i;j;j-=j&-j) printf("%d ",j); printf("\n"); } return 0; }
Output:
1: 1
2: 2
3: 3 2
4: 4
5: 5 4
6: 6 4
7: 7 6 4
8: 8
这段代码只是将以前的i+=lowbit(i)修改成了i-=lowbit(i)
再对比以前原图,查询A1到A8,只须要返回C8,查询A1到A7,只须要返回C7,C6,C4与咱们代码运行的效果一致,这就为咱们向下查询提供了条件。
四.树状数组应用
再回头看以前引出树状数组的题目,这时候就能够有必定的思路了。
树状数组主要的函数分为两个,即更新函数和查询函数。
void fix(int x) { int i; for(i=x;i<=n;i+=i&-i) //向上更新
e[i]++; //一维树状数组e
} //注意fix()的形参值x<=0时死循环。
int getsum(int x) { int ret=0,i; //返回值为ret,初值为0
for(i=x;i;i-=i&-i)//向下查询
ret+=e[i]; return ret; } //注意getsum()的形参值x<0时e[ ]数组越界。
void fix(int x) { int i; for(i=x;i;i-=i&-i) //向下更新
e[i]++; } int getsum(int x) { int ret=0,i; for(i=x;i<=n;i+=i&-i)//向上查询
ret+=e[i]; return ret; }
树状数组能够有两个方向,1.向下更新,向上查询 2.向上更新,向下查询。本题用的是向上更新,向下查询。
注意:树状数组更新时是增长量,初始时候更新量就是它自己。
因此开始时利用更新函数,将每个点更新,以后只要输出就好了。
代码:
#include<stdio.h>
int a[100005]; int c[100005]; int n; int lowbit(int x) { return x&(-x); } void add(int x,int ad) { for(int i = x;i<=n;i+=lowbit(i)) { c[i]+=ad; } } int getsum(int x) { int ans = 0; for(int i = x;i>0;i-=lowbit(i)) { ans+=c[i]; } return ans; } int main() { scanf("%d",&n); for(int i = 1;i<=n;i++) { scanf("%d",&a[i]); add(i,a[i]); } int m; scanf("%d",&m); for(int i = 1;i<=m;i++) { char s[2]; int x,y; scanf("%s%d%d",s,&x,&y); if(s[0]=='C') { add(x,y-a[x]); a[x] = y; }else { printf("%d\n",getsum(y) - getsum(x-1)); } } return 0; }
树状数组其余应用,以后会陆续补充。若对于其有新的理解,也会加入到其中。
更新时间(2018.12.6)