一,什么是树状数组。
树状数组作为线段树的一个分支,能处理一些特殊情况下的线段树问题,在数学上我们这样定义:
数据类型为 ,更新操作为将
变为
,查询为求
。
如果 构成一个幺半群,就可以用线段树实现。
如果 并且(S, +)构成一个交换群,就可以用树状数组实现。
数据类型为 ,更新操作为对所有的
将
变为
,查询为求
。
如果 构成一个幺半群,
构成一个幺半群,并且满足右分配律,即
,就可以用带懒标记的线段树实现。
树状数组(Binary Indexed Tree(B.I.T), Fenwick Tree)是一个查询和修改复杂度都为log(n)的数据结构。主要用于查询任意两位之间的所有元素之和,但是每次只能修改一个元素的值;经过简单修改可以在log(n)的复杂度下进行范围修改,但是这时只能查询其中一个元素的值(如果加入多个辅助数组则可以实现区间修改与区间查询)。
二,树状数组的原理及操作
首先我们从树开始说起。最普遍的二叉树如下,叶子节点a[1]~a[8]。
现在我们将另一个数组C[i]以某种关系插入到这个树中 得到
C[i]代表 子树的叶子结点的权值之和//
这里以求和举例
如图可以知道
C[1]=A[1];
C[2]=A[1]+
A[2]
;
C[3]=A[3];
C[4]=
A[1]+
A[2]+A[3]+A[4]
;
C[5]=A[5];
C[6]=A[5]+A[6];
C[7]=A[7];
C[8]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];
将C[]数组的结点序号转化为
二进制
1=(001)
C[1]=A[1];
2=(010)
C[2]=A[1]+
A[2]
;
3=(011)
C[3]=A[3];
4=(100)
C[4]=
A[1]+
A[2]+A[3]+A[4]
;
5=(101)
C[5]=A[5];
6=(110)
C[6]=A[5]+A[6];
7=(111)
C[7]=A[7];
8=(1000)
C[8]=
A[1]+
A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8]
;
这时我们可以发现C[i]对应的权值和与他所在的位数貌似存在一种对应关系
1=(001)
C[1]=A[1]; 1位的权值和
2=(010)
C[2]=A[1]+
A[2]
; 2位的权值和
3=(011)
C[3]=A[3]; 1位的权值和
4=(100)
C[4]=
A[1]+
A[2]+A[3]+A[4]
; 4位的权值和
5=(101)
C[5]=A[5]; 1位的权值和
6=(110)
C[6]=A[5]+A[6]; 2位的权值和
7=(111)
C[7]=A[7]; 1位的权值和
8=(1000)
C[8]=
A[1]+
A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8]
; 8位的权值和
在解释这种关系之前我们引入一个叫lowbit的东西
int lowbit(int t)
{
return t&(-t);
}
一个数t异或自己的负数,在计算机中,负数由补码表示,return一个数的原码 &自己的负数补码得到的数
那么这个数怎么用,这时有两种理解方法:
(1)
C[i]=A[i-2^k+1]+A[i-2^k+2]+......A[i]; (k为i的二进制中从最低位到高位连续零的长度)例如i=8时,k=3;
-t 代表t的负数 计算机中负数使用对应的正数的补码来表示
例如 :
t=6(0110)
此时 k=1
-t=-6=(1001+1)=(1010)
t&(-t)=(0010)=2=2^1 lowbit(t)=2^k 即K=1
C[i]=A[i-2^k+1]+A[i-2^k+2]+......A[i];
C[i]=A[i-lowbit(i)+1]+A[i-lowbit(i)+2]+......A[i];
(2)
第二种理解方法用到上文提到的n位权值和,n和C[i]所在的数位具有一定的关系:
1=(001)
C[1]=A[1]; 1位权值和(001) lowbit(1)=001
2=(010)
C[2]=A[1]+
A[2]
; 2位权值和(010) lowbit(2)=010
3=(011)
C[3]=A[3]; 1位权值和(001) lowbit(3)=001
4=(100)
C[4]=
A[1]+
A[2]+A[3]+A[4]
; 4位..(100) lowbit(100)
5=(101)
C[5]=A[5]; 1位...(001) lowbit(5)=001
6=(110)
C[6]=A[5]+A[6]; 2位...(010) lowbit(6)=010
7=(111)
C[7]=A[7]; 1位...(001) lowbit(7)=001
8=(1000)
C[8]=
A[1]+
A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8]
; 8位...(1000) lowbit(8)=1000
那么此时lowbit()所代表的就是n的最低位1所代表的2进制数,同时也说明了此时C[i]为几个A[i]的加和
操作:
区间查询:
下面利用C[i]数组,求A数组中前i项的和
单点更新:
举个例子 i=7;
sum[7]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7] ; 前i项和
C[4]=A[1]+A[2]+A[3]+A[4]; C[6]=A[5]+A[6]; C[7]=A[7];
可以推出: sum[7]=C[4]+C[6]+C[7];
序号写为二进制: sum[(111)]=C[(100)]+C[(110)]+C[(111)];
再举个例子 i=5
sum[7]=A[1]+A[2]+A[3]+A[4]+A[5] ; 前i项和
C[4]=A[1]+A[2]+A[3]+A[4]; C[5]=A[5];
可以推出: sum[5]=C[4]+C[5];
序号写为二进制: sum[(101)]=C[(100)]+C[(101)];
细细观察二进制 树状数组追其根本就是二进制的应用
int getsum(int x)
{
int ans=0;
for(int i=x;i>0;i-=lowbit(i))
ans+=C[i];
return ans;
}
对于i=7 进行演示
7(111)
ans+=C[7]
lowbit(7)=001 7-
lowbit(7)=6(110) ans+=C[6]
lowbit(6)=010 6-lowbit(6)=4(100) ans+=C[4]
lowbit(4)=100 4-lowbit(4)=0(000)
对于i=5 进行演示
5(101)
ans+=C[5]
lowbit(5)=001 5-
lowbit(5)=4(100) ans+=C[4]
lowbit(4)=100 4-lowbit(4)=0(000)
当我们修改A[]数组中的某一个值时 应当如何更新C[]数组呢?
回想一下 区间查询的过程,再看一下上文中列出的图
void add(int x,int y)
{
for(int i=x;i<=n;i+=lowbit(i))
tree[i]+=y;
}
//可以发现 更新过程是查询过程的逆过程
//由叶子结点向上更新C[]数组
当更新A[1]时 需要向上更新C[1] ,C[2],C[4],C[8]
C[1], C[2], C[4], C[8]
写为二进制
C[(001)],C[(010)],C[(100)],C[(1000)]
1(001) C[1]+=A[1]
lowbit(1)=001 1+lowbit(1)=2(010)
C[2]+=A[1]
lowbit(2)=010 2+lowbit(2)=4(100) C[4]
+=A[1]
lowbit(4)=100 4+lowbit(4)=8(1000) C[8]
+=A[1]
三:树状数组的优缺点:
树状数组是一个可以很高效的进行区间统计的数据结构。在思想上类似于线段树,比线段树节省空间,编程复杂度比线段树低,但适用范围比线段树小。相比线段树的优势:空间复杂度略低,编程复杂度低,容易扩展到多维情况。劣势:适用范围小,对可以进行的运算也有限制,比如每次要查询的是一个区间的最小值,似乎就没有很好的解决办法。
相比于线段树而言,线段树的二分思想运用的更为深刻而树状数组更类似于前缀区间的模型: