树状数组
假设一个数可以xxx可以被二进制分解成x=2i1+2i2+...+2imx = 2^{i_1} + 2^{i_2} + ... + 2^{i_m}x=2i1+2i2+...+2im,不妨设i1>i2>...>imi_1 > i_2 > ... > i_mi1>i2>...>im,进一步地,区间[1,x][1, x][1,x]可以分成O(log x)O(log\ x)O(log x)个小区间:
- 长度为2i12^{i_1}2i1的小区间[1,2i1][1, 2^{i_1}][1,2i1]
- 长度为2i22^{i_2}2i2的小区间[2i1+1,2i1+2i2][2^{i_1} + 1, 2^{i_1} + 2^{i_2}][2i1+1,2i1+2i2]
- .........
- 长度为2im2^{i_m}2im的小区间[2i1+2i2+...+2im−1+1,2i1+2i2+...+2im][2^{i_1} + 2^{i_2} + ... + 2^{i_{m - 1}} + 1, 2^{i_1} + 2^{i_2} + ... + 2^{i_m}][2i1+2i2+...+2im−1+1,2i1+2i2+...+2im]
这些小区间的共同特点是:若区间结尾为RRR,则区间长度为lowbit(R)lowbit(R)lowbit(R)。
树状数组,就是基于上述思想的数据结构,其基本用途是维护序列的前缀和。
对于给定的序列aaa,建立数组ccc,其中c[x]c[x]c[x]的含义是以xxx为结尾的,长度为lowbit(x)lowbit(x)lowbit(x)的区间中所有数的和,即∑i=x−lowbit(x)+1xa[i]\sum_{i = x - lowbit(x) + 1}^{x} a[i]∑i=x−lowbit(x)+1xa[i]。

单点更新:
void add(int x, int t) {
for (int i = x; i <= n; i += lowbit(i)) c[i] += t;
}
区间查询:
int ask(int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += c[i];
}
对于查询:
我们想要得知以区间[1,x][1, x][1,x]的和,不妨从树状数组区间长度的划分和数组ccc的定义出发。我们知道,对于长度为xxx的区间,我们可以每次将其划分为长度为lowbit(x)lowbit(x)lowbit(x)的区间,并x=x−lowbit(x)x = x - lowbit(x)x=x−lowbit(x)。所以,我们很容易得到查询的代码。
复杂度是O(log n)O(log\ n)O(log n)。
对于更新:
除树根外,每个内部节点c[x]c[x]c[x]的父节点是c[x+lowbit(x)]c[x + lowbit(x)]c[x+lowbit(x)]。
复杂度是O(log n)O(log \ n)O(log n)。
树状数组的扩展应用
以上介绍了树状数组的基本应用,即:
- 修改数组中的一个数。
- 查询区间和。
树状数组还有一些扩展应用,如:
扩展一:
- 将区间[l,r][l, r][l,r]加上ccc。
- 查询某个位置上的数。
扩展二:
- 将区间[l,r][l, r][l,r]加上ccc。
- 查询区间和。
扩展三: 结合其他算法(如:二分)实现更多功能
考虑扩展一
对于给定的序列aaa,考虑其差分数组bbb。
若想将区间[l,r][l, r][l,r]加上一个数,可以对应在差分数组上执行b[l]=b[l]+cb[l] = b[l] + cb[l]=b[l]+c,b[r+1]=b[r+1]−cb[r + 1] = b[r + 1] - cb[r+1]=b[r+1]−c。
若想查询某个位置xxx的数,即求位置xxx的前缀和。
以上两个操作是单点修改,区间查询。可以用树状数组解决。
考虑扩展二
对于区间给定的序列aaa,考虑其差分数组bbb。
若想将区间[l,r][l, r][l,r]加上一个数,可以对应在差分数组上执行b[l]=b[l]+cb[l] = b[l] + cb[l]=b[l]+c,b[r+1]=b[r+1]−cb[r + 1] = b[r + 1] - cb[r+1]=b[r+1]−c。
若想查询区间[1,r][1, r][1,r]的和,即∑i=1r∑j=1ibj\sum_{i = 1}^r \sum_{j = 1}^{i}b_j∑i=1r∑j=1ibj
对应代码为:
for (int i = 1; i <= r; i ++)
for (int j = 1; j <= i; j ++)
ans += b[j];
考虑以下表格
| a1a_1a1 | b1b_1b1 | |||
|---|---|---|---|---|
| a2a_2a2 | b1b_1b1 | b2b_2b2 | ||
| a3a_3a3 | b1b_1b1 | b2b_2b2 | b3b_3b3 | |
| ......... | ||||
| aia_iai | b1b_1b1 | b2b_2b2 | ......... | bmb_mbm |
a1=b1a_1 = b_1a1=b1
a2=b1+b2a_2 = b_1 + b_2a2=b1+b2
a3=b1+b2+b3a_3 = b_1 + b_2 + b_3a3=b1+b2+b3
.........
ai=b1+b2+...+bma_i = b_1 + b_2 + ... + b_mai=b1+b2+...+bm
则a1+a2+...+ai=(b1+b2+...+bm)×(i+1)−(b1+2×b2+3×b3+...+m×bm)a_1 + a_2 + ... + a_i = (b_1 + b_2 + ... + b_m) \times (i + 1) - (b_1 + 2 \times b_2 + 3\times b_3 + ... + m\times b_m)a1+a2+...+ai=(b1+b2+...+bm)×(i+1)−(b1+2×b2+3×b3+...+m×bm)
可以看出,其转化成了bbb数组的前缀和,以及i×b[i]i\times b[i]i×b[i]数组的前缀和。可以用树状数组维护。
考虑扩展三
考虑如下问题,给定for (i=1;i≤n;i++) cnt[i]=1for\ (i = 1; i \leq n; i ++) \ cnt[i] = 1for (i=1;i≤n;i++) cnt[i]=1 表示111到nnn中所有数均可用。
现需要:每次找到所有可用数中第kkk小的数,将该数存下,并将该数标记为不可用。
考虑维护数组cntcntcnt的前缀和数组,标记为不可用只需将1←01 \leftarrow 01←0。
前缀和每次可以算出有多少可用的数,然后可以二分。
所以该问题需要的操作是:单点修改和区间查询。配合二分可以实现更多功能。
1059

被折叠的 条评论
为什么被折叠?



