树状数组基础

树状数组,是一种可以在 O(logn)O(\log n) 的时间内,实现单点修改和查询前缀和的操作数据结构。

如何实现呢?我们先从查询前缀和开始。

若一个正整数 xx 的二进制形式可以被表示为 bk1bk2b1b0b_{k - 1}b_{k - 2}\dots b_1b_0,其中等于 11 的位有 {bi1,bi2,,bim}\{b_{i_1}, b_{i_2}, \cdots, b_{i_m}\}xx 可以被分解为 2i1+2i2++2im2^{i_1} + 2^{i_2} + \cdots + 2^{i_m} 的形式。

然后,查询一个前缀和就相当于查询区间 [1,x][1, x] 的和。

i1>i2>>imi_1 > i_2 > \cdots > i_m,则区间 [1,x][1, x] 可以被划分为 mm 个不重叠的区间:

[1,2i1][1, 2^{i_1}]

[2i1+1,2i1+2i2][2^{i_1} + 1, 2^{i_1} + 2^{i_2}]

[2i1+2i2+1,2i1+2i2+2i3][2^{i_1} + 2^{i_2} + 1, 2^{i_1} + 2^{i_2} + 2^{i_3}]

\cdots

[2i1+2i2++2im1+1,x][2^{i_1} + 2^{i_2} + \cdots + 2^{i_{m - 1}} + 1, x]

x=2i1+2i2++2imx = 2^{i_1} + 2^{i_2} + \cdots + 2^{i_m}

这些区间的长度分别为 2i1,2i2,,2im2^{i_1}, 2^{i_2}, \cdots, 2^{i_m}

设第 ii 个区间的结尾为 rir_i,则第 ii 个区间的长度为 lowbit(ri)\operatorname{lowbit}(r_i)

树状数组就使用了这种思想。具体来讲,对于一个需要维护的序列 aa,建一个序列 sssis_i 的值为 j=ilowbit(i)iai\sum_{j = i - \operatorname{lowbit}(i)}^i a_i

假如我们要查询 [1,7][1, 7] 的前缀和。

[1,7][1, 7] 可以被划分为 33 个区间:[1,4],[5,6],[7,7][1, 4], [5, 6], [7, 7]

如图,我们首先从 s7s_7 开始,s7=[7,7]s_7 = [7, 7],把结果加上 s7s_7

我们从 s7s_7 往前跳 lowbit(7)=1\operatorname{lowbit}(7) = 1 步,跳到 s6s_6s6=[5,6]s_6 = [5, 6],把结果加上 s6s_6

我们从 s6s_6 往前跳 lowbit(6)=2\operatorname{lowbit}(6) = 2 步,跳到 s4s_4s4=[1,4]s_4 = [1, 4],把结果加上 s4s_4

我们从 s4s_4 往前跳 lowbit(4)=4\operatorname{lowbit}(4) = 4 步,跳到 s0s_0,发现 00 已经不被 [1,7][1, 7] 包含,返回结果。

可以写出查询前缀和的代码:

int query(int p){
	int sum = 0;
	while (p) sum += s[p], p -= lowbit(x);
	return sum;
}

单点增加,就是修改 axa_x,并正确维护 ss 数组。显然,我们只需要将 ss 数组内包含 axa_x 的项增加。

可以写出单点修改的代码:

void update(int p, int x){
	while (p <= n) s[p] += x, p += lowbit(p);
}

模板题一代码(查询 [l,r][l, r] 时只需查询 [1,r][1,l1][1, r] - [1, l - 1] 即可):

#include <algorithm>
#include <cstdio>

using namespace std;

int n, m, t[500001], tt, ttt, tttt;
int lowbit(int x){
	return x & -x;
}
void add(int i, int x){
	while (i <= n){
		t[i] += x;
		i += lowbit(i);
	}
}
int sum(int i){
	int s = 0;
	while (i > 0){
		s += t[i];
		i -= lowbit(i);
	}
	return s;
}
int main(){
	scanf("%d%d", &n, &m);
	for (int i = 1;i <= n;i ++) scanf("%d", &tt), add(i, tt);
	for (int i = 1;i <= m;i ++){
		scanf("%d%d%d", &tt, &ttt, &tttt);
		if (tt == 1) add(ttt, tttt);
		else printf("%d\n", sum(tttt) - sum(ttt - 1));
	}
}

树状数组的技巧

值域树状数组

我们可以将树状数组作为一个桶来使用。

具体的讲,就是每一次新增一个值 xx 时,执行 update(x, 1) 的操作;每一次删除一个值 xx 时,执行 update(x, -1) 的操作。利用树状数组的查询前缀和功能,我们可以快速回答,有多少个数的值域在 [l,r][l, r] 中这样的问题。

当值域过大时,我们可以使用离散化来将数映射到 [1,n][1, n] 的范围内,再进行下一步操作。

树状数组上倍增

当在使用树状数组的时候,我们经常会应对一些答案有单调性的问题(比如在值域树状数组上寻找第 kk 小)。

一种显然的解决方案就是使用二分,但是会增加一个 log\log 的时间复杂度,时间复杂度为 O(log2n)O(\log^2 n)

有没有一种不增加时间复杂度的解决方案呢?当然是有的。

由于树状数组的每一个节点存储的都是一个长度为 22 的整次幂的区间和,我们可以使用同样思想的倍增算法。

我们以值域树状数组上寻找第 kk 小为例。

我们设两个变量 ansanssumsum,都初始化为 00

然后,我们可以从 logn\lfloor \log n \rfloor 倒序枚举 pp,对于每一个 pp,如果 ans+2pnans + 2^p \le nsum+sans+2p<ksum + s_{ans + 2^p} < kansans+2pans \to ans + 2^psumsum+sans+2psum \to sum + s_{ans + 2^p}

最后,ans+1ans + 1 即为第 kk 小的值。由于倍增与树状数组相结合,可以直接在 ss 数组内查询,免去了调用查询操作的 log\log,时间复杂度为 O(logn)O(\log n)

倍增查询第 kk 小代码:

int kth(int k){
	int p = 1, ans = 0, sum = 0;
	for (;(p << 1) <= n;p <<= 1);
	while (p){
		if (ans + p <= n && sum + s[ans + p] < k) ans += p, sum += s[ans];
		p >>= 1;
	}
	return ans + 1;
}

树状数组的拓展

区间修改单点查询

树状数组所擅长的是单点修改区间查询,如果我们把操作换成区间修改单点查询,该怎么办呢?

我们可以使用差分来解决这个问题。

aa 的差分数组 bb 满足以下条件:

  1. b1=a1b_1 = a_1
  2. 对于 2in2 \le i \le nbi=aiai1b_i = a_i - a_{i - 1}

我们可以发现,j=1ibj=ai\sum_{j = 1}^i b_j = a_i。这样一来,我们想查询 aia_i 的时候,只需要查询 bb 数组的前缀和即可。

那修改怎么办呢?

我们假设 n=8n = 8,要将 [2,5][2, 5] 加上 xx

a{a1,a2+x,a3+x,a4+x,a5+x,a6,a7,a8}a \to \{a_1, a_2 + x, a_3 + x, a_4 + x, a_5 + x, a_6, a_7, a_8\}

b{b1,b2+x,b3,b4,b5,b6x,b7,b8}b \to \{b_1, b_2 + x, b_3, b_4, b_5, b_6 - x, b_7, b_8\}

我们可以发现,对于差分序列 bb,我们只需要使 blbl+xb_l \to b_l + xbr+1br+1xb_{r + 1} \to b_{r + 1} - x 即可。

模板题二代码:

#include <cstdio>

using namespace std;

int n, m;
long long t[500001], ss, l, r;
char c;
long long lowbit(long long x){
	return x & -x;
}
long long sum(int i){
	long long s = 0;
	while (i > 0){
		s += t[i];
		i -= lowbit(i);
	}
	return s;
}
void add(int i, long long x){
	while (i <= n){
		t[i] += x;
		i += lowbit(i);
	}
}
int main(){
	scanf("%d%d", &n, &m);
	for (int i = 1;i <= n;i ++) scanf("%lld", &r), add(i, r - l), l = r;
	for (int i = 1;i <= m;i ++){
		scanf(" %c", &c);
		if (c == '1') scanf("%d%d%lld", &l, &r, &ss), add(l, ss), add(r + 1, -ss);
		else scanf("%lld", &l), printf("%lld\n", sum(l));
	}
}

区间修改区间查询

考虑在区间修改单点查询的基础上继续升级。

如果单点查询 axa_x 的结果为 i=1xbi\sum_{i = 1}^x b_i,那查询区间 [1,x][1, x] 的结果即为:

i=1xj=1ibj\sum_{i = 1}^x \sum_{j = 1}^i b_j

上式可以改写为:

i=1x(xi+1)×bi=(x+1)i=1xbii=1xi×bi\sum_{i = 1}^x (x - i + 1) \times b_i = (x + 1)\sum_{i = 1}^x b_i - \sum_{i = 1}^x i \times b_i

我们增加一个树状数组用来维护 i=1xi×bi\sum_{i = 1}^x i \times b_i,就可以用两个树状数组来实现区间修改区间查询了!(查询 [l,r][l, r] 还是只需查询 [1,r][1,l1][1, r] - [1, l - 1] 即可)

模板题三代码:

#include <algorithm>
#include <cstdio>
#define ll long long
inline int lowbit(int x){return x & -x;}
struct BIT{
	int n;
	ll s[100001];
	void init(int x){
		n = x;
		for (int i = 1;i <= n;i ++) s[i] = 0;
	}
	void update(int p, ll x){
		while (p <= n) s[p] += x, p += lowbit(p);
	}
	ll query(int p){
		ll sum = 0;
		while (p) sum += s[p], p -= lowbit(p);
		return sum;
	}
}t1, t2;

using namespace std;

int n, m, op, l, r;
ll x;
int main(){
	scanf("%d%d", &n, &m);
	t1.init(n), t2.init(n);
	for (int i = 1;i <= n;i ++){
		scanf("%d", &r);
		t1.update(i, r - l), t2.update(i, 1ll * (r - l) * i);
		l = r;
	}
	while (m --){
		scanf("%d", &op);
		if (op == 1){
			scanf("%d%d%lld", &l, &r, &x);
			t1.update(l, x), t1.update(r + 1, -x);
			t2.update(l, 1ll * l * x), t2.update(r + 1, 1ll * (r + 1) * -x);
		}
		else {
			scanf("%d%d", &l, &r);
			ll L = 1ll * l * t1.query(l - 1) - t2.query(l - 1);
			ll R = 1ll * (r + 1) * t1.query(r) - t2.query(r);
			printf("%lld\n", R - L);
		}
	}
}

例题

【模板】树状数组 1

【模板】树状数组 2

【模板】线段树 1

逆序对

楼兰图腾

三元上升子序列

Lost Cows