数据结构是个好东西…我希望我能学的会
脑子也是个好东西…我希望我有
目录
一、概念
树状数组或者二叉索引树也称作Binary Indexed Tree,又叫做Fenwick树;它的查询和修改的时间复杂度都是log(n),空间复杂度则为O(n),这是因为树状数组通过将线性结构转化成树状结构,从而进行跳跃式扫描。通常使用在高效的计算数列的前缀和,区间和。
其中a数组就是原数组,c数组则是树状数组,可以发现
C1 = A1
C2 = A1+A2
C3 = A3
C4 = A1+A2+A3+A4
C5 = A5
C6 = A5+A6
C7 = A7
C8 = A1+A2+A3+A4+A5+A6+A7+A8
对照式子可以发现 C[i]=A[i-2^k+1]+A[i-2^k+2]+......A[i]; (k为i的二进制中从最低位到高位连续零的长度)例如i=8时,k=3;
二、原理
将树状数组转换成二进制(嗯...二进制是个好东西)
转换成二进制后可以发现,树状数组的节点深度其实就是它的节点编号的二进制形式中,从右往左数第一个1出现的位置。
例如:
6的二进制形式中(110),从右往左数第一个1出现的位置是2
lowbit(6)=010=2
7的二进制形式中(111),从右往左数第一个1出现的位置是1lowbit(7)=001=1
8的二进制形式中(1000),从右往左数第一个1出现的位置是4lowbit(8)=1000=8
那么现在引入久lowbit(x) ,lowbit(x) 其实就是取出x的最低位1 ,换言之,lowbit(x)=2^k
所以 C[i]=A[i-lowbit(i)+1]+A[i-lowbit(i)+2]+......A[i];
所以该结构满足以下性质:
1.每个内部结点c[x]保存以它为根的子树中所有叶结点的和
2.每个内部结点c[x]的子结点个数等于lowbit(x)的大小
3.除树根外,每个内部结点c[x]的父结点是c[x+lowbiet(x)]
4.树的深度为O(logN)
1、lowbit()函数
-t 代表t的负数,计算机中负数使用对应的正数的补码来表示
例如 :
t=6(0110) 此时 k=1
-t=-6=(1001+1)=(1010)
t&(-t)=(0010)=2=2^1
int lowbit(int t)
{
return t&(-t);
}
2、单点修改和区间查询
当修改一个结点之后,需要修改所有包含这个元素的结点,也就是说要修改一个元素就得修改这个元素及其父结点的值,由性质3易得
void add(int x,int v)
{
while(x<=len)
{
c[x]+=v;
x+=lowbit(x);
}
}
查询是为了得到a[1]+a[2]+a[3]+...a[x]的值
int query(int x)
{
int sum=0;
while(x>0)
{
sum+=c[x];
x-=lowbit(x);
}
return sum;
}
合并后
void add(int x,int v)
{
while(x<=len)
{
c[x]+=v;
x+=lowbit(x);
}
}
int ask(int x)
{
int sum=0;
while(x>0)
{
sum+=c[x];
x-=lowbit(x);
}
return sum;
}
int range_ask(int l, int r){ //区间求和
return ask(r) - ask(l - 1);
}
3、区间修改和单点查询
这里要用到差分的思想
创建一个差分数组c[],令c[i] = a[i] - a[i-1] (a[i] 表示原本的第i个数)
则a[i] = ( a[i] - a[i-1] ) + ( a[i-1] - a[i-2] ) + ...... + ( a[2] - a[1] ) +a[1]
= c[i] + c[i-1] + ...... + c[2] + c[1]
所以单点查询变成了区间求和那么区间修改怎么办呢 ?
我们看这样一个例子:
a 1 3 4 5 7 10
c 1 2 1 1 2 3
若我们令区间[2,4]加2,则
a 1 5 6 7 9 10
c 1 4 1 1 2 1
我们可以发现只有c[2]和c[5]的数值改变了,其实原理也很好想,区间内的前后元素差是不变的,只有(区间第一个元素与前一个元素的差) 和 (区间后第一个元素与区间末尾元素的差)改变了。所以区间修改问题变成了单点修改问题。
假如原来数组都为A[6]=0 0 0 0 0 0,如果要在[2,4]区间加上5,那么只需将数组变成0 5 0 0 -5 0 ,i<2是不受+5的影响的,受+5影响的只有i>=2,也因为i>4都受到了+5的影响,所以i>4之后的数都要-5,那么原来的前缀和也变成了单点查询。
也就是说这里运用了差分思想,假设原本的数据存在a数组中,那么c数组储存的就是c[i]=a[i]-a[i-1],如果c[1]=a[1],那么很明显
a[i]=c[i]+c[i-1]+c[i-2]+...+c[2]+c[1]。这样我们每次单点查询的时候只要加上c数组的前缀就可以了
int lowbit(int x)
{
return x&(-x);
}
void update(int x,int v)
{
while(x<=n){
c[x]+=v;
x+=lowbit(x);
}
}
int ask(int x)
{
int sum=0;
while(x){
sum+=c[x];
x-=lowbit(x);
}
return sum;
}
void range_update(int x,int y,int t)
{
update(x,t);
update(y+1,-t);
}
4、区间修改和区间查询
理解了上面的,这个也就很好理解啦
这里还是用到差分的思想,因为a[i]=sigma(c,i)
a[1]+a[2]+...+a[n]
= (c[1]) + (c[1]+c[2]) + ... + (c[1]+c[2]+...+c[n])
= n*c[1] + (n-1)*c[2] +... +c[n]
= n * (c[1]+c[2]+...+c[n]) - (0*c[1]+1*c[2]+...+(n-1)*c[n]) (式子①)
那么我们就维护一个数组c2[n],其中c2[i] = (i-1)*c[i]
每当修改c的时候,就同步修改一下c2,这样复杂度就不会改变,那么
式子①=n*sigma(c,n) - sigma(c2,n)
int lowbit(int x)
{
return x&(-x);
}
void update(int x,int v)
{
while(x<=n){
c1[x]+=v;
c2[x]+=v*x;
x+=lowbit(x);
}
}
int ask(int x)
{
int sum=0;
while(x){
sum+=(x+1)*c1[x]-c2[x];
x-=lowbit(x);
}
return sum;
}
void range_update(int x,int y,int t)
{
add(x,t);
add(y+1,-t);
}
int range_ask(int l, int r){ //区间求和
return ask(r) - ask(l - 1);
}
三、特点
注意树状数组能处理的是下标为1...n的数组,绝对不能出现下标为0的情况,因为lowbit(0)=0,会陷入死循环。