一、一维树状数组
树状数组是一种存储前缀和的数据结构
树状数组通过二进制来划分区间存储存储部分的区间和,得到一个管理数组c
若i的二进制表示末尾有k个连续的0,则c[i]存储的区间长度为,即
c数组的修改
修改一个a中的值,只需要修改与其有关的祖先节点,例如修改a[5]只需改动c[6]和c[8]
如何获取区间长度?
c[i]表示的区间长度实际上就是i末尾1和后面所有零表示的二进制数字。
对i取反+1,再使其和i进行 与(&)操作即可得到区间长度
因为计算机以补码存储数字,-i 的补码为i取反加一
int lowbit(int i)
{
return (-i)&i;
}
从而得到c[i]表示的区间长度。
直接前驱:c[i]的直接前驱为c[i-lowbit(i)]
直接后继:c[i]的直接后继为c[i+lowbit(i)]
前驱:c[i]的直接前驱、其直接前驱的直接前驱等,即c[i]左侧所有子树的根
后继:c[i]的直接后继、其直接后继的直接后继等,即c[i]的所有祖先
因为区间长是按二进制划分的,所以c[i+lowbit(i)]得到的一定是其父节点
c[i-lowbit(i)]得到的一定是左侧紧邻的子树的根(区间长度大一倍)
因此对一个点进行更新,只需要更改其所有祖先,即所有后继
void add(int i,int w)
{
for(;i<=n;i+=lowbit(i)) c[i]+=w;
}
前缀和的查询
因为每个数都可以表示为若干个2的幂次之和,那么按照其划分前缀和区间计算c的和即可做到查询前缀和。即只需要查询其全部的前驱。
例:7D=111B,则sum[7]=c[7]+c[6]+c[4] (找前驱的过程实际上就是抹末尾1的过程)
int sum(int i)
{
int s=0;
for(;i>0;i-=lowbit(i))
s+=c[i];
return s;
}
树状数组每次更改和查询的时间复杂度均为
二、多维树状数组
加套一层循环即可
void add(int x,int y,int w)
{
for(int i=x;i<=n;i+=lowbit(i))
for(int j=y;j<=n;j+=lowbit(j))
c[i][j]+=w;
}
int sum(int x,int y)
{
int s=0;
for(int i=x;i>0;i-=lowbit(i))
for(int j=y;j>0;j-=lowbit(j))
s+=c[i][j];
return s;
}
int subsum(int x1,int y1,int x2,int y2)
{
return sum(x2,y2)-sum(x1-1,y2)-sum(x2,y1-1)+sum(x1-1,y1-1);
}
树状数组的局限性
树状数组主要用于查询前缀和、区间和及点更新,对点查询、区间修改效率较低
点查询:普通数组需要,树状数组需要
区间查询:普通数组需要,树状数组需要
减法规则:树状数组只适用于满足减法规则的区间维护,当问题不满足减法规则,如最大最小值时,则不能采用树状数组维护。
注意点:树状数组只能从1开始存,否则更新0时会导致死循环