浅谈树状数组
Upd on 2025/10/11:删除了争议性前言。
Upd on 2025/10/13:删除末尾一些语句。
注:下文中的
概念
完了有点忘了怎么办。
本质就是将一个长度为
那么该怎么分每个节点管辖的信息呢?
这是被定义的,定义如下:
设 lowbit(k),而 lowbit(k) 的意思是 lowbit(5) 的计算方法就是先将 lowbit(x) 函数被实现为 & 运算,具体为什么在此不多赘述,可以自行查询位运算相关知识。
基本用法
终于到了激动人心的基本用法讲解!
写在前面
首先,你要了解树状数组查询区间信息的本质——差分。
因此,普通的树状数组不能维护区间最大值等等不可差分的信息!
区间求和
注意:树状数组每次只能查询形如下标在
假设我们要查询区间 lowbit(k) 为 lowbit 运算的值)。
首先,我们已经有了
之后,在求出了
代码:
int Query(int x) {
int ret=0;
for(; x; x-=lb(x)) {
ret+=c[x];
}
return ret;
}
单点修改
如果要修改
观察最初的图,可以发现 lowbitx(x)),那么就可以通过不断从
代码:
void Modify(int x,int v) {
for(; x<=m; x+=lb(x)) {
c[x]+=v;
}
}
Tips
注意,一般情况下,树状数组中的初值就是在原数组对应的位置存上对应的值!
练习
现在,你学习了树状数组的基本用法,那么,你就可以完成 P3374 了!
小进阶
1. 区间修改,单点查询
结合普通的差分数组,我们就可以实现区间修改,单点查询!
实现:
- 求出原序列的查分数组
b ,并将其按照下标作为键值存入树状数组。 - 利用原本树状数组“单点修改”的特性与差分性质,修改
b_{l} 与b_{r+1} 即可实现“区间修改”。 - 利用原本树状数组“区间查询”的特征与差分性质——对差分数组求前缀和就可以获得原序列可得,查询的第
x 个数的值相当于求出1 到x 在树状数组上的和。
代码(存储初值):
b[i] = a[i] - a[i - 1];
Modify(i, b[i]);
代码(修改):
Modify(l, k);
Modify(r + 1, -k);
代码(查询):
printf("%d\n", Query(x));
练习 2
接下来,完成 P3368 吧!
2. 区间修改,区间查询
其实当我们掌握了基本方法,做任意理论上可以实现的查询都可以通过“推式子”得出。
由上个部分的差分数组做考虑组合出答案。
首先,
那么这个式子可以分别维护左半部分和右半部分,开两个树状数组即可,一个存储普通的数值,一个存储
代码(存储):
t1.add(i,b[i]);
t2.add(i,(i-1)*b[i]);
代码(修改):
t1.add(x,k);
t1.add(y+1,-k);
t2.add(x,k*(x-1));
t2.add(y+1,-k*y);
代码(查询):
ans+=y*t1.q(y);
ans-=(x-1)*t1.q(x-1);
ans-=t2.q(y);
ans+=t2.q(x-1);
练习 3
使用树状数组完成 P3372。
正经进阶
来点进阶的!
1. 权值树状数组
本质上,权值树状数组就是将原本用下标作为键值的树状数组改为了用一个数值作为下标!
他有什么用呢?
他可以相当于将树状数组变成一个“数轴”,因为使用值域作为下标,那么原本在一个位置加上一个数的操作就相当于在一条数轴上标记一个点。
值得注意的是,这种方法受空间限制(总不能开
如果我们要统计一个数组中
根据上文的描述,我们可以将这些值全部作为点存进“数轴中”,假设为下图。
其中,红色数字代表树状数组中的“下标”,蓝色的数字表示这个数字出现的次数,那么,如果我们想要统计比 Query(5),如果要查询比某个数大的元素个数,也只需要求得这个数右侧的下标对应的蓝色数的和即可。
代码(加点,其中求
int rk=lower_bound(b+1,b+m+1)-b;
Modify(rk,1);
练习 4
好了,再结合离散化,你就可以完成:
- SP32899(通常情况下,“加点”操作都要按照顺序一个一个来,不能一次性全部加进去,因为这样会导致对下标的要求混乱,比如你要求所有的
sum_{l} \leq sum_{r} 为了保证l,r 的关系,你需要按照顺序加点)。 - P1908(本题中因为逆序对也要求了下标的顺序,注意控制“加点”的顺序与查询的细节)。
2. 全局第 k 大值。
再次考虑权值树状数组。
问题相当于:在权值树状数组上找到一个
二分
考虑更高效的“跳”来找到
想到倍增,从大到小枚举一个
由于
其次,修改操作只要在权值树状数组对应的映射上进行加减操作即可。
3. 二维树状数组
顾名思义,二维树状数组就是对一个矩阵进行操作的数据结构。
先给个代码感受下:
void add(int x, int y, int v) {
for (int i=x; i <= n; i += lowbit(i)) {
for (int j=y; j<=n; j += lowbit(j)) {
c[i][j] += v;
}
}
}
int sum(int x,int y) {
int ret=0;
for(int i=x; i; i-=lowbit(i)) {
for(int j=y; j; j-=lowbit(j)) {
ret+=c[i][j];
}
}
return ret;
}
具体讲解可以看作者的这篇题解。
习题 5
- SP1029。
- P4514。
后记
希望这篇专栏可以让你知道不一样的东西!