树状数组 Fenwick Tree / Binary Index Tree
基本数据结构
树状数组类似线段树,但实现代码更短一些(但有一些同学认为比线段树更难理解),可以支持方便的单点/欧战修改和单点/区域查询操作。
树状数组和线段树可以选择掌握,大部分题目两者都可以用(线段树更通用一些),一般来说两种方法保证掌握一个就可以了。
如上图示,定义 \(c[]\) 树状数组,来跟踪 \(a[]\) 的信息,使得在\(a\)上面做的查询,可以在\(c\)上更快的完成。比如\(c_2\)管理\(a_1,a_2\)。 可以看到节点覆盖的求和范围是有重叠的,所以做单点修改时,需要改动多个节点。
定义一个lowbit
函数,用来帮助我们定位\(c\)数组的下标:
// 返回二进制表示中最低的位:lowbit(0b10110000) == 0b00010000
// 这个计算方法来自补码:“取反加一”
int lowbit(int x) {return x & -x;}
树上的两个基本操作就是add()和getsum():
void add(int x, int k) {
while (x <= n) { // 不能越界
c[x] = c[x] + k;
x = x + lowbit(x);
}
}
int getsum(int x) { // a[1]..a[x]的和
int ans = 0;
while (x >= 1) {
ans = ans + c[x];
x = x - lowbit(x);
}
return ans;
}
单点修改,区间求和
用上面图中所示的标准定义的树状数组,就已经可以解决单点修改、区间求和的问题。
对照上面图中的数组范围,可以验证以上算法的正确性。
模板例题是LibreOJ 130: 树状数组模板题(单点修改,区间查询)
参考代码
#include <iostream>
#include <cstring>
using namespace std;
long long bit[1000010];
int n, q, act, l, r;
// x 的二进制表示中,最低位的 1 的位置。
// lowbit(0b10110000) == 0b00010000
// lowbit(0b11100100) == 0b00000100
// 这个计算方法来自补码:“取反加一”
int lowbit(int x) { return x & (-x); }
void update(int x, int k) {
while (x <= n) {
bit[x] += (long long)k;
x += lowbit(x);
}
}
long long getSum(int x) {
long long ans = 0;
while (x > 0) {
ans += bit[x];
x -= lowbit(x);
}
return ans;
}
int main() {
while (cin >> n >> q) {
memset(bit, 0, sizeof(bit));
for (int i = 1, x; i <= n; ++i) {
cin >> x;
update(i, x);
}
while (q--) {
cin >> act >> l >> r;
if (act == 2) {
cout << getSum(r) - getSum(l - 1) << endl;
} else {
update(l, r);
}
}
}
return 0;
}
区间修改、单点查询
如果我们的问题是区间修改的,那么我们可以把\(a\)定义成目标数组的差分值,这样就能把区间修改转化为起点、终点的差分值的单点修改,把单点查询转化为1到此点的区间查询操作。
例如LibreOJ 131: 区间修改、单点查询
参考代码
#include<stdio.h>
#define ll long long
ll c[1000010];
ll n,q;
ll lowbit(ll x)
{
return x&(-x);
}
void add(ll i,ll a)
{
while(i<=n)
{
c[i]=c[i]+a;
i=i+lowbit(i);
}
}
ll sum(ll x)
{
ll ans=0;
while(x>0)
{
ans=ans+c[x];
x=x-lowbit(x);
}
return ans;
}
int main()
{
ll i,a,b=0;
scanf("%lld %lld",&n,&q);
for(i=1; i<=n; i++)
{
scanf("%lld",&a);
add(i,a-b);
b=a;
}
while(q--)
{
ll s,l,r,x;
scanf("%lld",&s);
if(s==1)
{
scanf("%lld %lld %lld",&l,&r,&x);
add(l,x);
add(r+1,-x);
}
else if(s==2)
{
int h;
scanf("%lld",&h);
printf("%lld\n",sum(h));
}
}
}
区间修改、区间查询
再进一步,如果需要在区间修改的数组上做区间查询,那么我们还是维护差分值作为数组,但这时区间查询就变得更复杂一些。
若维护序列 \(a\) 的差分数组 \(b\),此时我们对 \(a\) 的一个前缀 \(r\) 求和,即 \(\sum_{i=1}^{r} a_i\),由差分数组定义得 \(a_i=\sum_{j=1}^i b_j\)
进行推导
我们用两个树状数组分别维护 \(\sum b_i\) 和 \(\sum (i b_i)\),就能实现区间求和。如下图所示:
例题LibreOJ 132:区间修改、区间查询
参考代码
int t1[MAXN], t2[MAXN], n;
inline int lowbit(int x) { return x & (-x); }
void add(int k, int v) {
int v1 = k * v;
while (k <= n) {
t1[k] += v, t2[k] += v1;
k += lowbit(k);
}
}
int getsum(int *t, int k) {
int ret = 0;
while (k) {
ret += t[k];
k -= lowbit(k);
}
return ret;
}
void add1(int l, int r, int v) {
add(l, v), add(r + 1, -v); // 将区间加差分为两个前缀加
}
long long getsum1(int l, int r) {
return (r + 1ll) * getsum(t1, r) - 1ll * l * getsum(t1, l - 1) -
(getsum(t2, r) - getsum(t2, l - 1));
}
习题
- P3253 [JLOI2013]删除物品 提高+/省选-
- P4085 [USACO17DEC]Haybale Feast G 提高+/省选-
- P6278 [USACO20OPEN]Haircut G 普及+/提高
- P4868 Preprefix sum 普及+/提高
- P1972 [SDOI2009]HH的项链 提高+/省选-
- CF703D Mishka and Interesting sum 省选/NOI-
- P6186 [NOI Online #1 提高组] 冒泡排序 提高+/省选-
- P3374 【模板】树状数组 1 普及/提高-
- P3368 【模板】树状数组 2 普及/提高-
- P1637 三元上升子序列 普及+/提高
- P2357 守墓人 普及/提高-
- P4145 上帝造题的七分钟 2 / 花神游历各国 提高+/省选-