引言
树状数组的目的:快速求前缀和,修改某一个数
普通数组:快速求前缀和 O ( n ) O(n) O(n),修改某一个数 O ( 1 ) O(1) O(1)
树状数组:快速求前缀和,修改某一个数 都是 O ( l g n ) O(lgn) O(lgn)
树状数组的示意图
树状数组的特点在于,利用了二进制的性质,可以很方便的实现查询和修改。
通过上图我们可以清晰的看出,关键点在于每个索引转为为二进制以后的lowbit是多少,每个数组维护的区间和的区间长度恰好是lowbit,区间是从(index-lowebit, index]
,注意左开右闭。
主要由三个核心的函数构成
函数 lowbit(x)
主要是为了找到最低1位,因此lowbit(x) = x&-x
查询函数 query(x)
查找区间[1, x]
的区间和。注意是从1开始的。这一步我们需要对所有与x
相关的位置的值进行加和,也就是x的二进制中,为1的那些位置。
def query(x):
res = 0
while x>0:
res += tree[x]
x -= lowbit(x)
return res
添加函数 add(x, value)
对某个值进行修改,并且还需要修改与之相关的全部的位置维护的区间和。相关位置是指x+lowbit(x)
def add(x, value):
n = len(tree)
while x < n:
tree[x] += value
x += lowbit(x)
例题1,移动k次能够得到的最小数字
题目的特点是,有n个可以且只能利用一次的数字,每次使用完会对下一次使用其他数字时候产生影响。因此可以考虑设置树状数组,每次利用query查找到使用该数字的代价,使用之后update数组,维护新的数组中其他元素的使用代价。
例题2,图腾问题
题目需要明显得到某个数字左右两侧小于自己的数字个数,我们可以考虑一个巧妙的方法,对于数字大小的比较,我们直接把数字自身作为索引,在数组左侧的数字,都是该数字在原数组左侧中小于它的数字;在数组右侧的数字,都是该数字在原数组左侧中大于它的数字。正反两侧遍历就可以得到结果。
典型特征在于,统计了数字的个数。
一般树状数组的题目没有很明显的特征,不容易让人想到用树状数组。还是需要多做题积累经验。
这道题的特点在于,我们考虑把每个数字转化为坐标,我们在当前坐标时,只需要考虑已经遍历的部分有多少大于当前位置的,有多少小于当前位置的。通过正反两次遍历,可以找到对于每一个位置时,左右两侧有多少大于/小于的数字。最后只需要乘起来就可以。
说实话,这个想法很难想到。
def lowbit(x): # 计算得到最低的1位
return x & -x
def add(