【数据结构】树状数组

树状数组

假设一个数可以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+...+2im1+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=xlowbit(x)+1xa[i]

image-20250518143140939

单点更新:

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=xlowbit(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)


树状数组的扩展应用

以上介绍了树状数组的基本应用,即:

  1. 修改数组中的一个数。
  2. 查询区间和。

树状数组还有一些扩展应用,如:

扩展一:

  1. 将区间[l,r][l, r][l,r]加上ccc
  2. 查询某个位置上的数。

扩展二:

  1. 将区间[l,r][l, r][l,r]加上ccc
  2. 查询区间和。

扩展三: 结合其他算法(如:二分)实现更多功能

考虑扩展一

对于给定的序列aaa,考虑其差分数组bbb

若想将区间[l,r][l, r][l,r]加上一个数,可以对应在差分数组上执行b[l]=b[l]+cb[l] = b[l] + cb[l]=b[l]+cb[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]+cb[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_ji=1rj=1ibj

对应代码为:

for (int i = 1; i <= r; i ++) 
	for (int j = 1; j <= i; j ++) 
		ans += b[j];

考虑以下表格

a1a_1a1b1b_1b1
a2a_2a2b1b_1b1b2b_2b2
a3a_3a3b1b_1b1b2b_2b2b3b_3b3
.........
aia_iaib1b_1b1b2b_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;in;i++) cnt[i]=1 表示111nnn中所有数均可用。

现需要:每次找到所有可用数中第kkk小的数,将该数存下,并将该数标记为不可用。

考虑维护数组cntcntcnt的前缀和数组,标记为不可用只需将1←01 \leftarrow 010

前缀和每次可以算出有多少可用的数,然后可以二分。

所以该问题需要的操作是:单点修改和区间查询。配合二分可以实现更多功能。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值