树状数组(是数组而不是树)
用途:单点修改与区间求和,区间修改与单点取值(线段树都可以实现,不过显得大材小用)
树状数组利用二进制的思想,以上图为例,红色代表树状数组bit[],黑色代表原生数组arr[],则可直观看出
bit[1]=arr[1];
bit[2]=arr[1]+arr[2];
…
bit[6]=arr[5]+arr[6];
…
bit[12]=arr[9]+arr[10]+arr[11]+arr[12];
…
bit[14]=arr[13]+arr[14]
…
简单找规律(查资料)后发现,C[i] = A[i - 2k+1] + A[i - 2k+2] + … + A[i]; //k为i的二进制中从最低位到高位连续零的长度,即i的二进制表示中最后一位1及后面所有的0组成的数。
那么2k该怎么求呢?
参考本文第八个小知识点,可知2k即为i中最小的2的幂,而该知识点中判断的方法正好可以用来获得i的二进制表示中最后一位1及后面所有的0组成的数,即(i&(-i)).
//需要先构造lowbit函数或宏
//宏
#define lowbit(x) (x&(-x))
//函数
int lowbit(int x){
return (x&(-x));
}
单点修改与区间求和
构造树状数组:
arr[i] 包含于 bit[i + 2k]、bit[(i + 2k) + 2k]…
所需空间为线性,bit[max_num]即可(构造全局变量)
随数据的输入直接构造即可(相当于原数据均为0,然后每次单点增加arr[i]大小的数据,因此,单点修改有着与之一样的操作):
void update(int index,int delta,int num){
while(index<=num){
bit[index]+=delta;
index+=lowbit(index);
}
}
...
for(int i=1;i<=num;i++){
cin>>arr[i];
update(i,arr[i],num);
}
区间求和:
int query(int index,int num){
int ans=0;
while(index>0){
ans+=bit[index];
index-=lowbit(index);
}
return ans;
}
上述代码求解的是 $ \sum_{i=1}^{index} $(arr[i]),要想求解区间x~y范围内的数据和,只需query(y)-query(x-1).
区间修改与单点取值
与单点修改不同,在区间修改的过程中,若是按上述过程对区间中的数据分别修改,则完全体现不出树状数组的优势,因此这里引入差分的概念。
差分
对于序列a1,a2,a3…an,对它进行差分就变成了a1 ,a2-a1 ,a3-a2…,an-an-1,差分后的序列的前i项和
sum=a1+(a2-a1)+(a3-a2)+…+(ai-1-ai-2)+(ai-ai-1)=ai
即从1到 i - 1 项都被消掉了,最后只剩下了ai,得出结论:差分后序列的前 i 项和等于差分前第 i 个元素的值,利用此性质再结合上面的query()函数,则可以快速进行单点取值。
那么差分在区间修改过程中有什么奇妙的应用呢?
由上述结论可以推出当原数组的连续区间[L,R]被统一修改时,其差分数组只需要修改index=L和index=R+1的两个数据,原因如下:
原因如下
-
区间[L,R]内每个元素都加上了相同的值,所以相对差距并没有改变,所以[L+1,R]的差分值并没有改变,但是这个区间的两头却会发生改变;
-
第L个元素加上了x,但第L-1个元素没有加x,所以第L个元素的差分结果就会从原来的(aL-aL-1)变成了现在的(aL-aL-1+x),增加了x;
-
同理尾部第R个元素加上了x,但是第R+1个元素没有加x,所以第R+1个元素的查分结果就从原来的(aR+1-aR)变成了现在的(aR-aR-1-x),减少了x.
void update(int index,int delta,int num){ while(index<=num){ bit[index]+=delta; index+=lowbit(index); } } int last=0,cur; for(int i = 1; i <= num; i++) { cin>>cur; update(i, cur - last, n); last = cur; }
洛谷树状数组板子练习: