树状数组笔记

树状数组笔记

树状数组相比于线段树类的操作,支持单点修改与区间查询,代码量小于线段树类的一种精简算法

普通的树状数组可以维护满足结合律且可以差分运算的信息

树状数组的每个元素表示它管辖范围内的元素总值,(前缀和,乘积,异或等)

相比于线段树的每个父节点管辖两个儿子,树状数组允许一个父节点管辖多个儿子节点

img

*图片引自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[x]+=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];//累加区间总值
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[x]+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[x]+=k;
x+=lowbit(x);
}
return;
}
long long query(long long s[],long long x){
long long t=0;
while (x){
t+=s[x];
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]

posted @   才瓯  阅读(6)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示