对于一段区间,前缀和数组和普通数组对 单点修改和区间查询 两种操作支持的复杂度
单点修改: 前缀和数组 ,普通数组
区间查询: 前缀和数组,普通数组
当单点修改和区间查询的操作都比较频繁时,运行时间取决于最坏的操作,N次操作复杂度达
对于任一段区间,我们可以转化成对和的操作。因此考虑模型时,先搞清楚.
对于区间,长度为。对于数,借用二进制拆分的思想,可以把拆成二进制上单独的的加和。
如:
套用到区间上,我们同样可以把区间按照区间长度拆成或者说
如果求的区间和,可直接由各个子区间的和推出来,我们只需要知道子区间的区间和即可
考虑到各个方面,树状数组拆区间虽然也是和二进制位有关,但是为了计算(后面提到),并不是按照这个顺序拆的。
⭐
对于拆好的区间,可以发现区间长度和区间右端点之间有关系,在这之前先定义一个操作
:返回二进制下最末位的那个
如:
的实现
为求出,最朴素的做法: 从最后一位向前寻找 ,令该位和按位相与,得1则立即返回 1<<(第几位)
如果对计算机对变量的二进制表示比较熟悉,了解补码的形式,则就能看懂这个表述形式
int lowbit(int x){ return x&(-x); }如&
&
注意:
这里所说的返回的 ‘“,不是单纯返回个1,而是说返回二进制表示下的末位的位数 对应的十进制数
也可以等价成(指的是末尾连续0的数目):
关于操作的叙述到此为止,我们接着对区间的性质分析
区间的长度为
区间的长度为
区间的长度为
看上去我们发现了一个令人震惊的真相,实际上这在我们拆区间的时候已经决定好了
我们拆区间时,是按照的二进制位从低位到高位依次拆的,计算的时候也即是
, ,……
上式也改写成
通过我们可以快速将一段区间拆成子区间
上面讨论了将区间按照二进制拆分所相关的性质。
接着说,如何利用这些东西进行区间查询和单点修改
区间查询: 通过二进制枚举子区间,得出大区间
我们把原数组记为,的每一个下标对应的则对应着单点的值,(这是废话,无需多言
引入一个辅助数组,就像引入前缀和数组一样。定义为储存数组的一段区间和
或者写做
同时我们还发现,数组下标存储的值还有着联系,如下图
以为例,,包含的区间长度为
减去自己对应的这一区间,得
按照二进制划分区间
即可有,注意的定义和所指的区间的长度
上面的讨论是说,求数组时,不必蛮力运算。直接根据下标的关系可以推知
区间查询
求的前缀和
int sum(int x){ int res=0; for(int i=x;i;i-=lowbit(i)) res+=C[i]; return res; }任何区间都可以这样,通过划分好的子区间求得
于是进行区间查询时,我们先将其转化位两个前缀和样式的区间。再通过区间划分,由子区间推出大区间,即可求得。
单点修改
例如,我们要修改,发现在上图中直接包含的数组为,于是修改即可
直接包含,再通过修改,依次向上进行修改,这种包含关系就像一颗树一样,每个数只被一个数直接包含
也即可得出时间复杂度为
具体如何快速找到,划分区间时是以为长度划分的,定位父区间时,加上向回推进即可
代码
void add(int x,int val){ for(int i=x;i<=n;i+=lowbit(i)) C[i]+=val; }建立树状数组的时候,也只要对每个单点进行修改就行
void init(){ for(int i=1;i<=n;i++){ add(i,A[i]); } }
这是树状数组的基操
单点可以看做长度为的区间,但是区间修改怎么办呢?
看到区间修改,会想到差分的做法。其实树状数组+差分就可以完成区间修改这一做法,但是意味着 不再支持区间查询
我们再创建一个差分数组,树状数组维护的信息
现在树状数组还是进行单点修改和区间查询的操作,但是操作对象换成了
- 进行单点修改,回想一下差分是如何做到给区间加和的,给区间加上值,只需:+=,-=。我们发现 完全可以用树状数组达到
- 进行区间查询,对差分求前缀和,即得到原数组,这也是完全的
代码不表(滑稽
看起来比前面复杂许多,实际上就是复杂许多
考虑到区间修改操作,我们仍然计划让树状数组维护差分,现在开始分析性质
为了方便,记原数组为,差分数组为,树状数组为
那么很明显:,现要计算前缀和
如果画图,可以发现的值实际上是三角形
我们要算的也就是红色部分圈出来的值,假设我们要运算的是区间里的值
发现,规律难寻,或者说,很难转化成树状数组求解
现在将其补全为一个矩阵的形式

那么可以得到:
我们可以通过维护两个树状数组,一个维护另一个维护,通过二者的组合得出前缀和
此外,树状数组还有一些其他的应用,举几个例子
1.二维树状数组
2.如何时间复杂度建树状数组
但是,我们有大杀器线段树,何必难为自己呢