能用一个二进制数分出 个小区间:从 递归下去。利用这种思想,对于一个数组 ,可以构造出一个数组 ,其中 。树状数组就是这样一个数组。
单点修改,单点求值
树状数组的基本模型是单点修改和求和。
要修改
,就要修改掉所有包含
的
。根据
的公式,只有所有
且
的
满足条件。因此可以使用 i += lowbit(i)
不断获取下一个要更新的节点。
要查询
,就按照上面分区间的思想,使用 r -= lowbit(r)
不断获取下一个要累计的区间。
上面两个操作的时间复杂度均为 。
inline int lowbit(int x){
return (-x) & x;
}
void add(int k, int v){
while (k <= n)
c[k] += v, k += lowbit(k);
}
int query(int r){
int res = 0;
while (r > 0)
res += c[r], r -= lowbit(r);
return res;
}
初始化
初始化一个树状数组可以使用依次单点更新的方法,这样做是 的。有两种更好的线性的初始化方式。
一种是利用 管理的区间为 ,先处理出前缀和然后直接做。
另一种不需要额外空间,只需要简单的递推即可。代码如下所示。
for (int i = 1; i <= n; ++i)
a[i] = c[i];
for (int i = 2; i <= n; i <<= 1)
for (int j = i; j <= n; j += i)
c[j] += c[j - (i >> 1)];
区间修改,单点求值
维护原来数列的差分数列即可。
区间修改,区间求值
还是维护原来数列的差分数列。设
,则
。因此有
所以另外维护一个 的树状数组即可。
int c1[100005], c2[100005], n;
void add(int r, int k){
for (int i = r; i <= n; i += lowbit(i))
c1[i] += k, c2[i] += k * r;
}
ll query(int r){
ll res = 0;
for (int i = r; i > 0; i -= lowbit(i))
res += 1ll * (r + 1) * c1[i] - c2[i];
return res;
}
求第 小值
仿照权值线段树的思想,我们构建权值树状数组,然后在权值树状数组上倍增。下面的 表示原序列被离散化成 的数有多少个。
我们现在要找第 小的值,也就是找到最小的下标 ,满足 。这可以转化成找到最大的下标 使得 ,那么结果就是 。这一点和倍增很像。
而后的过程和倍增更像:我们从大到小枚举 bit,利用树状数组的性质检查上面那一点,如果成立就加入这个 bit。时间复杂度 。
int kth(int k){
int cnt = 0, ret = 0;
for (int i = 18; i >= 0; --i)
if (ret + (1 << i) < n && cnt + c[ret + (1 << i)] < k)
ret += (1 << i), cnt += c[ret];
return ret + 1;
}
典型例题如 POJ 2182。
树状数组与时间戳
在 CDQ 分治等算法中,树状数组需要被频繁地更新和重置。这带来的时间成本是很高的。
因此可以对树状数组的每一个下标维护一个时间戳,如果在更新某一个下标时发现时间戳不是正在使用的,那么就重置该下标的值;如果是在询问时发现,那就不计入该下标的答案。
int D, tag[100005];
inline int lowbit(int x){
return (-x) & x;
}
void add(int k, int v){
while (k <= n){
if (tag[k] != D) tag[k] = D, c[k] = 0;
c[k] += v, k += lowbit(k);
}
}
int query(int r){
int res = 0;
while (r > 0){
if (tag[r] == D) res += c[r];
r -= lowbit(r);
}
return res;
}
二维树状数组
树状数组可以很方便地放到二维上。修改的时候两个维度都跑一次加法即可,前缀和查询类似。时间复杂度均为 。
其他
树状数组好像还可以用来处理区间最值问题,但是貌似没有线段树方便好写,所以就没有看了。
但是树状数组还是可以比较方便的用来处理前缀最值相关的问题的。典型例题如 SCOI2014 方伯伯的玉米田。