为了完成 计算右侧小于当前元素的个数,我们需要先了解一个全新的数据结构——树状数组。
考虑以下需求,对于一个数组,我们需要两个方法:
add(idx, delta)
我们需要对 arr[idx]
,加上一个 delta
值。
ask(idx)
返回 arr[0:idx+1]
的总和。
我们容易想到下列两种解决方案
解决方案1——直接使用传统数组:
这种方案下,add()
方法的时间复杂度是 ask()
方法的时间复杂度是
解决方案2——使用前缀和数组:
前缀和数组是指,有这样一个数组 pre
,其长度和传统数组 arr
一致。pre[i]
储存的是 sum(arr[0:i+1])
。
这种方案下,add()
方法的时间复杂度是 ask()
方法的时间复杂度降低到了
不管是上述解决方案中的哪种,在面对 n 次随机操作时,其期望的时间复杂度都是
那有没有什么办法可以将这两种操作的时间复杂度都变得很低呢?这就要使用树状数组了。
我们先来了解一个前置知识,什么是 lowbit。
lowbit 定义为非负整数 n 在二进制表示下最低位的 1 及其后面的所有的 0 的二进制构成的数值。
例如:
数字 | 二进制表示 | lowbit |
---|---|---|
1 | 0001 | 1 |
2 | 0010 | 10 |
3 | 0011 | 1 |
4 | 0100 | 100 |
5 | 0101 | 1 |
为了快速求得数字 n 的 lowbit 值,我们可以使用 n ^ ~n + 1
。也即 n 取反加一,再和原 n 的值按位与。由于计算机里负数用的是补码表示负数,那么我们直接用 n ^ -n
即可求出 lowbit(n)
下面来正式介绍树状数组(图片来自视频)
如图所示,这就是一个树状数组结构。
t[x]
保存的是以 x
为根的子树中所有叶节点的和。这一点和 pre[x]
不同,t[x]
并没有直接保存前缀和。例如 t[3]
就只保存了 arr[3]
。t[x]
保存了包括 arr[x]
在内的,往左一共 lowbit(x)
个数字的和。也就是说,t[x]
只管得到 lowbit(x)
个数字。在这种情况下,add()
和 ask()
方法都惊人地达到了
下面给出代码:
'''
假设有树状数组 bitArr,其长度为 n
bitArr for Binary Indexed Tree Array
'''
def add(idx, delta):
while idx < n:
bitArr[idx] += delta
idx += lowbit(idx)
def ask(idx):
v = 0
while idx >= 1:
v += bitArr[idx]
idx -= lowbit(idx)
return v
结合上面的图片,我们来理解一下为什么代码是这样的
例如我们要运行 add(2, 1)
那首先我们要另 t[2] += 1
,然后,我们需要让受到影响的父节点 t[4] += 1
,然后再另父节点的父节点 t[8] += 1
可以发现,前缀和数组 pre
中,add(idx, delta)
,会影响到 idx
之后的所有节点,所以是
另外,我们也知道了为什么要更新 idx += lowbit(idx)
,因为这是在找它的父节点。
例如我们要运行 ask(7)
根据图片,我们要凑齐 t[7] + t[6] + t[4]
才能凑出 pre[7]
。那我们是怎么知道应该这么凑的呢?前文提到,t[x]
只能管得到包括 x
的往左一共 lowbit(x)
个数字的和,所以每次循环我们都更新 idx -= lowbit(idx)
,因为减去 lowbit(idx)
,正好就到了 idx
管不到的地方,这样一直加下去就能凑齐了。
树状数组的应用:
单点更新
直接 add(idx, delta)
区间更新
假设有这么一种情况,要求将 arr[i:j+1]
范围内的数字都加上 delta
,应该怎么办?
我们引入一个增量数组 b
,初始情况下 b
为一个全零的数组。我们对 b
构造树状数组 bitArr
,其初始情况是全零数组。
此时,要求将 arr[i:j+1]
的范围内数字都加上 delta
,则 add(i, delta)
add(j+1, -delta)
,则 ask(idx)
就是 arr[idx]
的增量。这是因为,要对 [i, j]
都加上 delta
,那么就使得从 i
开始的前缀和加上 delta
即可,并且在 j + 1
开始,这个增量 delta
失效。