树状数组引入
一个数总可写成:$num=2^i + 2^j + 2^k,i<j<k$(参考二进制)
因此可以将[1,num]区间分成
$len=2^i:[1,2^i]$
$len=2^j:[2^i+1,2^j]$
$len=2^k:[2^j+1,2^k]$
树状数组(还有块状数组)就是这样将一个区间分成不同长度(一般长为2的幂次方)来进行维护的方法
- 树状数组也叫 Binary Indexed Tree,二进制索引树,树状数组里某个元素管理了原始输入数组多少数据是由下标决定的
- 树状数组通常用于动态的维护前缀数组
- 树状数组的特点是区间查询和单点更新均为
O(logn)
lowbit
先看一个例子:lowbit(44)=lowbit(101100B)=(100B)=4
可以发现
1 | 原码:101100 |
所有,lowbit(i)=i&(~i+1)
考虑到计算机以补码的形式存储整数,所以lowbit(i)=i&(-i)
- 当x为0时结果为0
- x为奇数时,结果为1
- x为偶数时,结果为x中2的最大次方的因子
树状数组和原数组
数组数组与原数组关系:(注意数组数组下标从1开始)
1 | C[1]=A[1] |
总结规律,可以发现
C[i]=A[i-lowbit(i)+1] + …… + A[i]
单点更新,区间查询
更新时,需要同时更新A[i],A[i+lowbit(i)],A[i+2*lowbit(i)]……不超过最大值
求和时,则累加A[i]+A[i-lowbit(i)]+A[i-2*lowbit(i)]+……+A[1]
例如,add(3,5),需要寻找父节点,同时对A[3],A[4],A[8]做+5,可以发现3+lowbit(3)=4,4+lowbit(4)=8
ask(7)时,需要寻找左上节点,同时对A[7],A[6],A[4]做累加,发现7-lowbit(7)=6,6-lowbit(6)=4
区间更新,单点查询
用树状数组维护一个差分数组b
【l,r】+d:add(l,d) and add(r+1,-d)
查询a[x]:ans=a[x]+ask[x]
ask[x]即为a[x]的增量
区间更新,区间查询
用2个数状数组维护
代码模板
1 | class FenwickTree: |
或者这个由力扣官方题解给出的版本:
1 | class BIT: |
以及我这个版本[🐕]
- 本体
1 | def lowbit(i): |
- DLC
1 | def update(self, index: int, val: int) -> None: # set nums[i]=val |
离散化
考虑到「树状数组」的底层是数组(线性结构),为了避免开辟多余的「树状数组」空间,需要进行「离散化」;
「离散化」的作用是:针对数值的大小做一个排名的「映射」,把原始数据映射到 [1, len] 这个区间,这样「树状数组」底层的数组空间会更紧凑,更易于维护
1 | # 去重方便离散化 |
参考文章
https://leetcode-cn.com/problems/count-of-smaller-numbers-after-self/solution/shu-zhuang-shu-zu-by-liweiwei1419/
https://www.cnblogs.com/xenny/p/9739600.html
区域和检索 - 数组不可变
这题可以直接用前缀和做
1 | class NumArray: |
区域和检索 - 数组可修改
树状数组维护前缀和,直接用前缀和会超时
1 | class NumArray: |
面试题 10.10. 数字流的秩
朴素的想法就是用cnt[x]记录x的个数,x的秩就是sum(cnt[:x])
直接模拟会超时,所以用树状数组维护cnt
1 | class StreamRank: |
计算右侧小于当前元素的个数
上来就是一个单调栈,没有意外直接WA了
1 | # 错误代码示范 |
因为单调栈的性质,只能用来求Next greater element,而非计数
这里应该用离散化+树状数组:
1 | class Solution: |
统计作战单位数
3元组问题,枚举中间点
直接枚举的时间复杂度为$O(n^2)$,显然不够好
用树状数组维护,可以达到O(nlogn)
1 | class Solution: |
总结
树状数组是维护区间的一个工具
你应该先想到一个结果数组,然后再设计树状数组去维护他