Intro
为什么要学习树状数组:写起来快、简单。
但是树状数组的代码其实并不直观,曾经我花了很长时间学习,理解补了放弃了,直接学了线段树,现在再回过头来认真学习了树状数组,发现emmmmm,这种数据结构真的很神奇也很优雅。
本文约8千字,阅读完可以学到:
1.树状数组的原理、实现
2.如何使用树状数组解决“单点修改区间查询“,“区间修改单点查询”,”区间修改区间查询“,”区间最值“等问题以及一些例题的代码
介绍与原理
树状数组,顾名思义是一个数组,通常用于解决区间问题,比如区间和。
给定一个数组,长度为,支持两种操作:
1. 将更新为
2.求区间和
第一个操作实现起来很简单,直接改掉即可,重点在第二个操作,在暴力的情况下,复杂度为
此时树状数组就可以出马,数状数组首先是一个数组,数组中的每个元素维护着一个区间的区间和。从含义上讲,它维护的区间长的像一颗二叉树。
假设数组长度
第一个重点来了,
- 这和
的二进制表示有关,比如的维护区间是,的维护区间是,的维护区间则是。
- 每个
。
那么只需要知道每个
- 不那么显然:
比如,
计算一个数字
lowbit(x) = x & (~ x + 1 ) = x & (-x)
结论:
有了上述结论,我们进入第二个重点
如何使用
- 原问题转换,
,问题转换成对于任何,计算
- 如何计算
?我们怎么知道区间和是由哪些维护的?
这就是树状数组强大的地方,让我们首先找到
至此,我们理解了如何使用数状数组计算单点修改的区间和。
区间修改稍微复杂一点,后面会有介绍。
实现
- 初始化
直接把
memset
更新操作
即把
我们知道
从上图可知,如果更新了
-
,更新,它维护区间一定包含,下一个区间的左端点是,此区间对应的右端点(也就是下标)是多少呢?
设右端点,对应的区间长度是,则这个表达式成立
这就是知道区间右端点,去计算左端点的逆操作,一个减掉,一个加上
我们把加上它的就完成了:,更新。 -
,加上和第一步同样的计算方式
-
,加上,更新
void
求和操作
我们已经发现,更新操作是从
求和操作是
那么代码也就很简单了
int
对于任意
并且求和
进阶
区间最值
考虑以下问题
给定数组
- 把
更新为
- 求区间
中的最大值
我们把区间求和问题进阶一下,变成了区间最值问题(最小值同理),对应的,数状数组的定义发生了改变。
遗憾的是,不能简单地更改求和函数来达到目的。求和函数中,不同区间是彼此不向交的,而最值的性质则需要利用区间之间的“包含”关系。
初始化和求和一样,全部赋0即可,再调用update(pos, val)更新。
- 更新
更新了
最值要求的是所有
上图是
这就是个找规律题:设给定
void
可惜
已知
否则表示
例如,我们要求
-
:,,
-
:,,
-
:,,
-
:,,
-
:结束
可以观察到,红色区间都是放弃求值的,最终求的只有黄色部分
int
最小值也同理,因此我们可以使用树状数组解决区间和、区间最值、区间极值等问题。
整个数列第k大
TODO
区间修改
之前说过,树状数组对于区间修改的问题基本无力,但需要一些技巧。
线段树倒是很适合这个问题,不过需要使用懒惰标记来实现,挺麻烦的。
给定数组
- 将区间
元素加上
- 求
常规的树状数组肯定不行了,所以我们需要引入差分数组
查分数组
因此
于是我们用树状数组
- 区间更新单点询问
对于区间更新操作,在
更新后变成
只有两项发生了变化
于是,区间
那么
void
为什么回答
- 区间修改区间查询
根据差分数组的定义我们可以得到如下表达式
那么就能推出
我们令一个前缀和系数数组
这样,前项由树状数组维护的
但是 注意,
-
-
和也代表的是前缀和,当我们给加上时,意味着给要加上,并更新影响的前缀和
综上,代码如下
void
习题讲解
- 区间求和模版题
题目地址 注意:各组测试样例间数据不能清0
单点更新,区间查询的模版题,可以直接上区间更新代码,要注意两点
- 单点更新并不是直接
,而是,因此我们要实现单点查询,再更新update(i, x-A[x])和update(i+1, A[x]-x)
- 数据证明,树状数组是真的快。
- 单点修改区间查询的树状数组:748ms 代码
- 区间修改区间查询的树状数组:794ms 代码
- zkw线段树:826ms 代码
- 题目链接 地址
在数轴上给定
显然暴力
当然了还有其他地方需要考虑:速度离散化处理,还需要维护某段区间上被加的个数等。
AC代码