树状数组 ,顾名思义就是像树一样的数组,当然这里说的是存储方式像树,就像哈希散列表根据哈希函数来储存。
具体怎么存呢。定义每一列的顶端结点C[]数组
图中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[i]代表的意义。我们其实就是把区间和存在一颗满二叉树上,为什么要这么存呢
这就要引入lowbit函数了。
lowbit 函数
int lowbit(int x) {return x&(-x);}
#define lowbit(x) x&(-x) ///宏定义形式
lowbit函数可以说是树状数组的精髓和核心了吧。它的作用是“二进制数从低位向高位数,第一个数字1"作为运算结果。比较难理解,看下例子好了:lowbit(12) = 4,12的二进制为1100,从低位到高位,碰见第一个1时,截断,只取下面部分作为结果,即:100,十进制是4。 这就体现了,二进制和位运算的奇妙之处了。刚开始接触,可能很难接受和理解。但是慢慢就懂了。
现在知道了这个函数,再看看下面这些,就应该知道为什么用满二叉树来储存了吧
将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]=A[i-2^k+1]+A[i-2^k+2]+......A[i]; (k为i的二进制中从最低位到高位连续零的长度)例如i=8时,k=3;
主要应用
区间查询:
由于我们的C[i]储存的[ i,i-lowbit(i)+1 ] 区间里每个数的和,为了方便求某段区间和,我们用一个数组sum [ x ]来存
区间 [ 1, x ]的和,由于[ x, y ]=[1, y] - [1 , x] 我们就能很方便得出任一区间的和了。
这里给出求Sum[ ] 的函数。
int sum(int c[],int i){
int s=0;
while(i>0){
s+=c[i];
i-=lowbit(i);
}
return s;
}
单点更新后的区间维护;
直接上代码吧,不难理解。
void updata(int i, int val)
{
while (i <= n)
{
c[i] += val;
i += lowbit(i);
}
}
/**************************************/分隔
现在讨论一下区间最值查询和维护。区间最值和区间和虽然不一样,但思想上大致相同。
在求区间最值的算法中,用h[x]来储存[x,x-lowbit(x)+1]中每个数的最大值。
求区间最值的算法中还有一个a[i]数组,表示第i个数是多少。
区间最值的查询:没办法照搬区间和的方法,
假设query(x,y)为[ x , y ]上的最大值, 上面也可以知道h[ y] 代表[y,y-lowbit(y)+1]的最大值。
那么
若y-lowbit(y) > x ,则query(x,y) = max( h[y] , query(x, y-lowbit(y)) );
若y-lowbit(y) <=x,则query(x,y) = max( a[y] , query(x, y-1);
用递归的思想来求解,代码中,我们常常用递推来求解。理解了上面这两个式子,基本上就没有问题了。
int query(int x, int y)
{
int ans = 0;
while (y >= x)
{
ans = max(a[y], ans);
y --;
for (; y-lowbit(y) >= x; y -= lowbit(y))
ans = max(h[y], ans);
}
return ans;
}
更新后的区间维护
void updata(int x,int data)
{
while (x <= n)
{
h[x] = data;//从左边开始维护
for (int i=1; i<lowbit(x); i<<=1)
h[x] = max(h[x], h[x-i]);
x += lowbit(x);
}
}