能用一个二进制数分出 O ( log n ) O(\log n) O(logn) 个小区间:从 [ x − lowbit ( x ) + 1 , x ] [x - \operatorname{lowbit}(x) + 1, x] [x−lowbit(x)+1,x] 递归下去。利用这种思想,对于一个数组 a [ i ] a[i] a[i],可以构造出一个数组 c [ i ] c[i] c[i],其中 c [ x ] = ∑ i = x − lowbit ( x ) + 1 x a [ i ] c[x] = \sum _{i=x - \operatorname{lowbit}(x) + 1}^{x} a[i] c[x]=∑i=x−lowbit(x)+1xa[i]。树状数组就是这样一个数组。
单点修改,单点求值
树状数组的基本模型是单点修改和求和。
要修改
a
[
i
]
a[i]
a[i],就要修改掉所有包含
a
[
i
]
a[i]
a[i] 的
c
[
x
]
c[x]
c[x]。根据
c
[
x
]
c[x]
c[x] 的公式,只有所有
x
≥
i
x\ge i
x≥i 且
lowbit
(
x
)
≥
lowbit
(
i
)
\operatorname{lowbit}(x) \ge \operatorname{lowbit}(i)
lowbit(x)≥lowbit(i) 的
x
x
x 满足条件。因此可以使用 i += lowbit(i)
不断获取下一个要更新的节点。
要查询
∑
i
=
1
r
a
[
i
]
\sum_{i=1}^{r} a[i]
∑i=1ra[i],就按照上面分区间的思想,使用 r -= lowbit(r)
不断获取下一个要累计的区间。
上面两个操作的时间复杂度均为 O ( log n ) O(\log n) O(logn)。
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;
}
初始化
初始化一个树状数组可以使用依次单点更新的方法,这样做是 O ( n log n ) O(n\log n) O(nlogn) 的。有两种更好的线性的初始化方式。
一种是利用 c [ x ] c[x] c[x] 管理的区间为 ( x − lowbit ( x ) , x ] (x - \operatorname{lowbit}(x), x] (x−lowbit(x),x],先处理出前缀和然后直接做。
另一种不需要额外空间,只需要简单的递推即可。代码如下所示。
for (int i = 1; i <= n; ++i)
c[i] = a[i];
for (int i = 2; i <= n; i <<= 1)
for (int j = i; j <= n; j += i)
c[j] += c[j - (i >> 1)];
区间修改,单点求值
维护原来数列的差分数列即可。
区间修改,区间求值
还是维护原来数列的差分数列。设
a
[
0
]
=
0
a[0]=0
a[0]=0,则
d
[
i
]
=
a
[
i
]
−
a
[
i
−
1
]
,
a
[
i
]
=
∑
j
=
1
i
d
[
j
]
d[i] = a[i] - a[i - 1], a[i]=\sum_{j = 1}^{i}d[j]
d[i]=a[i]−a[i−1],a[i]=∑j=1id[j]。因此有
∑
i
=
1
r
a
[
i
]
=
∑
i
=
1
r
∑
j
=
1
i
d
[
j
]
=
∑
i
=
1
r
(
r
−
i
+
1
)
d
[
i
]
=
(
r
+
1
)
∑
i
=
1
r
d
[
i
]
−
∑
i
=
1
r
i
×
d
[
i
]
\sum_{i = 1}^{r} a[i] = \sum_{i = 1}^{r}\sum_{j = 1}^{i}d[j] = \sum_{i = 1}^{r} (r-i + 1)d[i] = (r+1)\sum_{i = 1}^{r} d[i] - \sum_{i = 1}^{r} i\times d[i]
i=1∑ra[i]=i=1∑rj=1∑id[j]=i=1∑r(r−i+1)d[i]=(r+1)i=1∑rd[i]−i=1∑ri×d[i]
所以另外维护一个 i × d [ i ] i\times d[i] i×d[i] 的树状数组即可。
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;
}
求第 k k k 小值
仿照权值线段树的思想,我们构建权值树状数组,然后在权值树状数组上倍增。下面的 a [ i ] a[i] a[i] 表示原序列被离散化成 i i i 的数有多少个。
我们现在要找第 k k k 小的值,也就是找到最小的下标 x x x,满足 ∑ i = 1 x a [ i ] ≥ k \sum_{i=1}^{x} a[i] \ge k ∑i=1xa[i]≥k。这可以转化成找到最大的下标 x x x 使得 ∑ i = 1 x a [ i ] < k \sum_{i=1}^{x} a[i] < k ∑i=1xa[i]<k,那么结果就是 x + 1 x+1 x+1。这一点和倍增很像。
而后的过程和倍增更像:我们从大到小枚举 bit,利用树状数组的性质检查上面那一点,如果成立就加入这个 bit。时间复杂度 O ( log n ) O(\log n) O(logn)。
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;
}
二维树状数组
树状数组可以很方便地放到二维上。修改的时候两个维度都跑一次加法即可,前缀和查询类似。时间复杂度均为 O ( log 2 n ) O(\log^2 n) O(log2n)。
其他
树状数组好像还可以用来处理区间最值问题,但是貌似没有线段树方便好写,所以就没有看了。
但是树状数组还是可以比较方便的用来处理前缀最值相关的问题的。典型例题如 SCOI2014 方伯伯的玉米田。