树状数组相比于线段树类的操作,支持单点修改与区间查询,代码量小于线段树类的一种精简算法
普通的树状数组可以维护满足结合律且可以差分运算的信息
树状数组的每个元素表示它管辖范围内的元素总值,(前缀和,乘积,异或等)
相比于线段树的每个父节点管辖两个儿子,树状数组允许一个父节点管辖多个儿子节点
*图片引自OIWIKI
初始化树状数组定义,这里以维护前缀和信息为例
int n,m;//n个数,m个询问
int a[N],s[N];//a存元素,s记录树状数组维护的信息
那么如何计算一个节点的父节点是谁呢?树状数组的核心在于爬链操作,利用lowbit进行运算
假设一个节点的编号为 $x$ ,那么它的父节点编号为 $x+lowbit(x)$
这里定义的 lowbit 不是常规定义的最低位 $1$ 所在的位数,而是这个 $1$ 与它后面所有 $0$ 组成的数,即 $2^k$
int lowbit(int x){
return x&-x;
}
对一个单点进行修改操作,那么就需要修改它的所有父节点
例如修改上图的 $a_5$,则需要爬链修改 $s_5,s_6,s_8$
void change(int x,int k){
while (x<=n){
s+=k;//累加计算区间总值
x+=lowbit(x);//向上传递父节点
}
}
区间查询时,我们查询到的值为前 $x$ 个元素的前缀和
例如查询 $a_7$ ,爬链查询到的值为 $s_7+s_6+s_4$
int query(int x){
int t=0;
while (x){
t+=s;//累加区间总值
x-=lowbit(x);//爬链
}
return t;
}
输出区间和:
cin>>n>>m;
for (int i=1;i<=n;i++) cin>>a[i];//读入原数组
void build(void){
for (int i=1;i<=n;i++){
change(i,a[i]);
}
}//初始化前缀和
int x,k;
cin>>x>>k;
change(x,k);//点修
int x,y;
cin>>x>>y;
cout<<query(y)-query(x-1)<<endl;//区查
利用两端的前缀和相减计算区间和
上面的操作利用前缀和,进行点修与区查功能,反之,也可以利用差分,进行区修与点查功能
设差分 $d_i=a_i-a_{i-1}$
$$ \implies \sum_{j=1}^id_j=a_i $$
所以
$$ a_{[x,y]}+k\iff d_{x}+k,d_{y+1}-k\\ \impliedby \sum_{j=1}^x d_j=a_x+k,\sum_{j=1}^yd_j=a_y+(x-y+1)*k,\sum_{j=1}^{y+1}d_j=a_{y+1}+(x-y+1)*k $$
int x,y,k;
cin>>x>>y>>k;
change(x,k);
change(y+1,-k);//区修
//树状数组中维护的差分信息
int x;
cin>>x;
cout<<a+query(x)<<endl;//点查
//查询到的值为树状数组的前缀和,差分+前缀和即为修改的值
下面考虑树状数组实现区修与区查,综合差分与前缀和
区间查询的推导公式,需要查询 $[1,r] $ 的区间和,使用差分构造等式
$$ \sum_{i=1}^r a_i=d_1+(d_1+d_2)+(d_1+d_2+d_3)+\cdots+\sum_{i=1}^r d_i $$
整理右式为
$$ =d_1*r+d_2*(r-1)+\cdots+d_{r-1}*2+d_r*1\\ =(\sum_{i=1}^rd_r)*r-(d_1*0+d_2*1+d_3*2+\cdots+d_r*(r-1)) $$
可以分成两个部分求值,通过构造两个树状数组维护查询前缀和 $s_1$ 维护 $d_i$ ,$s_2$ 维护 $d_i*(i-1)$
int n,m;
long long a[100010];
long long s1[100010],s2[100010];
void change(long long s[],long long x,long long k){
while (x<=n){
s+=k;
x+=lowbit(x);
}
return;
}
long long query(long long s[],long long x){
long long t=0;
while (x){
t+=s;
x-=lowbit(x);
}
return t;
}
那么查询 $[x,y]$ 的区间和可以转化为
$$ [query(s_1,y)*y-query(s_2,y)]-[query(s_1,x-1)*(x-1)-query(s_2,x-1)] $$
long long x,y,k;
cin>>x>>y>>k;
change(s1,x,k);
change(s1,y+1,-k);//维护s1
change(s2,x,k*(x-1));
change(s2,y+1,-k*(y+1-1));//维护s2
输出区间和时因为处理原数组的方式不同,所以方法存在两种
- 显式处理前缀和
long long pre[100010];
for (int i=1;i<=n;i++) cin>>a[i];
for (int i=1;i<=n;i++) pre[i]=pre[i-1]+a[i];
int x,y;
cin>>x>>y;
long long sum=query(s1,y)*y-query(s2,y)-query(s1,x-1)*(x-1)+query(s2,x-1)+pre[y]-pre[x-1];
cout<<sum<<endl;
这里的 $s_1,s_2$ 只是单纯地存储了差分的信息
- 隐式处理前缀和
for (int i=1;i<=n;i++) cin>>a[i];
build();
void build(void){
for (int i=1;i<=n;i++){
change(s1,i,a[i]);
change(s1,i+1,-a[i]);
change(s2,i,a[i]*(i-1));
change(s2,i+1,-a[i]*(i+1-1));
}
}
int x,y;
cin>>x>>y;
long long sum=query(s1,y)*y-query(s2,y)-query(s1,x-1)*(x-1)+query(s2,x-1);
cout<<sum<<endl;
这里将初始数组转换为树状数组的差分形式,相当于第一段代码中显式维护的前缀和数组
即 query(s1, y)*y - query(s2, y)
等价于前缀和 pre[y]
评论区(暂无评论)