本来以为背背板子就够用了的,发现有的时候会需要其中的一些东西。
原来树状数组也有自己的不可替代性。
但是像用树状数组做平衡树这种我确确实实不感兴趣。
当摸鱼写一些吧。
个人认为,树状数组是最能体现 OI 魅力的数据结构,它集简洁,巧妙,智慧与一身,我非常喜欢。
这个是记录向的,并不是教学向的,就是闲得没事瞎写的。
lowbit 函数
如果想要真正理解树状数组,我们需要说说这个用途不仅仅限于树状数组,还有假壮压骗分等等用途的 lowbit。
lowbit(x) 表示的是 x 在二进制表示下最低位的 1 和后边的所有 0 组成的数值。
通过这个我们可以快速锁定最低位的 1 并简单的进行这个最低位 1 的删除,而且这个东西是可以快速求解的。
来说一下我们通常遇见的形式是怎么来的。
我们设一个数二进制表示下第 k 位是 1, 0~k-1 位都是0。
先把这个数字取反,这个时候 0~k-1 的这一堆 0 就变成了 1, 第 k 位就变成了 0,这个时候我们再加一,前边的一堆 1 都会变成 0,最终这个进位的过程停止在 k 的位置,由于取反现在前边的位置都是恰好相反的,这个时候我们与原来的数字进行按位与就好了。
补码之下,取反 x 表示为 -1-x。
所以我们得到了最常见的形式:lowbit(x)=x&(-x);
非常的优美。
一维树状数组
这里涉及单点修改区间查询,区间修改单点查询,区间修改区间查询。
我们以最基本的单点修改区间查询做例子。
单点修改,区间查询
先给一张蓝书的图片吧,这个就是我们的树状数组的样子了。
注意是别人的图,我觉得这个图片棒极了,偷偷贴一下。
在树状数组中,\(tr[i]\) 表示区间 \([i-lowbit(i)+1,x]\) 中的信息并。
以下我们以动态维护单点加减一个数字,区间求和为例子。
这个信息并自然就是区间的和。
先考虑区间求和,先假定左端点是 1。
我们发现区间的左右端点都是数字(废话)。
而众所周知,一个数字是可以被二进制分解的(废话*2)。
所以我们就可以通过二进制分解把这个东西搞成 \(log n\) 段,我们便可以将其对应在树状数组的各段上边。
比如说我们要求 \([1,6]\) 的和,6 的二进制是 110, 我们就可以将其分解成 110 段和 100 段,对应了树状数组的 4 和 6,我们在图上可以明显感受到这一点。
每一次跳段我们使用 lowbit 函数就行了。
我们发现这个不会大于 \(log\) 次的,跳的次数永远是二进制表示下 1 的数量,这也是树状数组速度极端优秀的根本原因。
如果左端点不是一我们就做差就行了,[a,b]=[1,a-1]-[1,b] 这个样子把答案差分出来就行了。
但这种查询方式让我们只能直接维护可以被差分的信息,大大减少了这个东西的泛用性,比如不能直接维护区间最值(其实可以,但是比较复杂,我不会)。
至于单点的修改,我们发现改一个点只会影响到覆盖自己的点,我们就不停加自己的 lowbit 就行了,把每个祖先加上自己的值,这个明显也是 log 级别的。
区间修改,单点查询
我们使用差分就行了,很简单,这里不解释差分了。
依然以加法为例子。
我们把 \([a,b]\) 的加 val 转化为差分数组中的 d[a] 加上 val,d[b+1] 减去 val 就成为了之前的问题。
求一个点的值自然就是求差分数组的前缀和。
就没了。
区间修改,区间查询
我们还是利用前缀和与差分的知识。
我们把区间求和用差分数组列出来,默认从 1 开始了,如上原因,都一样的。
\(\sum_{x}^{i=1}\sum_{j=1}^{i}d[j]\)
这个玩意难优化,我们考虑每一个 d[j] 出现了多少次,对于一个 j 自然是出现了 (x-j+1) 次的,我们直接改为枚举 d[j],通过出现次数来计算,就成了如下形式。
\(\sum_{j=1}^{x}(x-j+1)d[j]\)。
我们发现这样子依然是十分甚至九分的低效,于是我们开始拆开式子。
\((x+1)\sum_{j=1}^{x}d[j]-\sum_{j=1}^{x}j\times d[j]\)
我们使用两个树状数组分别维护 d[j] 和 d[j]* j 的前缀和就好了。
经典的应用
-
逆序对
-
优化一些过程
-
维护最后的 1
二维树状数组
也是一样的,只不过我们多了一维。