关于树状数组的一些东西

本来以为背背板子就够用了的,发现有的时候会需要其中的一些东西。

原来树状数组也有自己的不可替代性。

但是像用树状数组做平衡树这种我确确实实不感兴趣。

当摸鱼写一些吧。

个人认为,树状数组是最能体现 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

二维树状数组

也是一样的,只不过我们多了一维。

posted @ 2025-10-02 15:22  BaiBaiShaFeng  阅读(10)  评论(0)    收藏  举报
Sakana Widget右下角定位