树状数组笔记
树状数组笔记
树状数组相比于线段树类的操作,支持单点修改与区间查询,代码量小于线段树类的一种精简算法
普通的树状数组可以维护满足结合律且可以差分运算的信息
树状数组的每个元素表示它管辖范围内的元素总值,(前缀和,乘积,异或等)
相比于线段树的每个父节点管辖两个儿子,树状数组允许一个父节点管辖多个儿子节点
*图片引自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}\)
所以
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] $ 的区间和,使用差分构造等式
整理右式为
可以分成两个部分求值,通过构造两个树状数组维护查询前缀和 \(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]\) 的区间和可以转化为
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]
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具