专题:树状数组
简介
树状数组是一种用来计算区间和(涉及元素修改)的高效数据结构,其查询(计算区间和)和更新(修改单个元素值)操作都能只有O(log(n))的时间复杂度。
问题分析
对于一个数组nums[], 有两种基本计算需求:
a. 修改其中某一个元素nums[i]:
update(i, new_val);b. 获取某一区间和:
range_sum(nums[a..b]);
通常满足以上2种需求有如下两种算法:
a. 直接在原数组
nums上计算;b. 通过前缀和数组
prefix_sums来计算(prefix_sums[i]=sum(nums[0..i]));
两种算法的时间复杂度对比如下:
| update(i, new_val) | 时间复杂度 | range_sum(i, j) | 时间复杂度 | |
|---|---|---|---|---|
原数组nums[] | nums[i] = new_val | O(1) | sum(nums[i..j]) | O(n) |
前缀和数组prefix_sums[] | prefix_sums[i..]中的每一个都需要更新 | O(n) | prefix_nums[j]-prefix_nums[i-1] | O(1) |
可以发现,上述两种不同算法对于update和range_sum这两种操作只能兼顾一个的性能。
而树状数组可以很好的对这两种操作进行平衡,将时间复杂度均达到O(log(n)),是二者的一个折衷。
树状数组由来
首先,对于普通数组A[],将其按二叉树进行组织如下:

转换一下:

定义每一列的顶端结点C[]数组

C[i]代表 子树的叶子结点的权值之和
如图可知
| |
将下标转换为二进制,并转化为递归式:
| |
树状数组C[]存在如下规律:
C[i]是A[..i]的部分元素和;C[i]和中一定包含A[i];C[i]和中一定不包含A[j](j>i);$ C[i] = A[i - 2k+1] + A[i - 2k+2] + … + A[i] $
其中:k为i的二进制中从最低位到高位连续零的长度
引入lowbit(x): x & -x , 用于快速计算x的最低位1开始截断的数($lowbit(x) = 2^k$), 有
| |
引入lowbit(x)后,通过lowbit(x)可快速递归计算和x有关的下标,主要有两种:
树左侧和
C[x]不相连的最近节点,其下标可通过x -= lowbit(x)依次访问, 这些节点不包含重复元素;树右侧和
C[x]相连的最近父节点,其下标可通过x += lowbit(x)依次访问,父节点包含了子节点;
借助lowbit(x)的这2种特性,可分别快速树状数组的两种操作流程:
修改操作:modify(i, diff)
任一
C[i]存储的是从nums[i]开始的部分nums[]和, 所以如果更新nums[i],则需要更新从C[i]开始到C[n]所有包含nums[i]的相关C[]节点的值;从
C[i]开始, 首先更新当前节点C[i]:C[i] += diff;通过
i += lowbit(i)计算上一级的C[i]下标, 更新上一级节点C[i] += diff;直到
i > nums.len(), 更新结束;
查询操作:prefix_sum(i)
prefix_sum(i)用于计算nums[0..i]前缀和;prefix_sum(i)为只和C[i]及前面的C[]有关;初始化累加和
prefix_sum = 0;从
i开始, 计算累加和prefix_sum += C[i];通过
i -= lowbit(i)计算下一个下标,计算累加和prefix_sum += C[i];直到
i==0, 查询结束;返回累加和
prefix_sum