树状数组
树状数组代替线段树。 —— zzz《常数优化与 NOILinux》
顾名思义,树状数组是一种树形结构,但是因为他独特的结构和拆分方式(二进制你又来了是吧),可以直接用数组存储。
用途是解决大部分区间修改查询问题,复杂度一般是
Update on 2023/6/7 本质上就是能够以极小的常数且严格
实际上原理看看就得了,这玩意板子写完了半年过去都不带忘的...
关于常数差距到底多大?S2 的同学们可以看看 S2OJ # 786,使用普通快读仅 2.60s(现在最快的我的提交记录是卡常版的),线段树解法貌似最快的是 lyq 学长的 6.69s。
基本树状数组
原理
来自学长的一张图
注意下文基本上所有东西都可以/需要搭配这个图进行理解

以求区间和的树状数组为例,树中每一个节点存储一段区间的和,询问时进行前缀和加减即可得出任意区间的和。
分区间的方法即利用二进制,对于第 1
的位置为
注意此处 0
开始算
以 0000 0011
,最低一位 1
的位置是 0,则
再以 0000 1000
,最低一位 1
的位置是 3,则
实现
建立
实际上不用特意处理建立,直接在输入过程中每输入一个数就修改即可。
1 | for (int i = 1; i <= n; ++i) { 复制 |
lowbit
本函数实现查找一个数最后一位
首先声明一个冷知识(大概冷),位运算是针对内存中存储的原数据的运算,换句话说,进行位运算的是补码而不是原码。
原码即一个数本身的二进制表示,反码即在原码的基础上符号位不变其他位取反。
补码分类来说,正数的补码是它的原码,负数的补码为其反码
在这里我们只考虑最后一位 1
及它后面的 0
。
比如 0011 0100
这个二进制数,我们先把它变成负数,即按位取反后加一(易证这就是他的相反数),变为 11001011
再变为 11001100
。
发现了一件神奇的事情,原来最低一位 1
竟然回来了!
显然,这位 1
前面的所有数字因为被取反所以和原数完全不同;后面的所有数字原来一定是 0
取反后变成一排 1
,负数的补码是反码1
这里,而它们本身变回了 0
;又因为这位 1
被取反后是 0
所以无论如何都不会再向前进位。
从而保证对于任意一个正数 1
是相同的。
因此 lowbit()
的实现就非常简单了,直接用 x&-x
,得到的结果就是原理中所说的
1 | int lowbit(int x) { 复制 |
单点修改
假设修改为给第
1 | void modify(int x, int v) { 复制 |
区间查询
查询区间为
因为
1 | int query(int x) { 复制 |
改区间查单点
上文我们解决了求前缀和的问题,本次需要查询单点,那么很容易联想到差分。更改区间对应改差分数组上的两个点,查询单点对应查询差分数组的前缀和。
区间加区间求和
发现树状数组好快于是回来补一下(2023.6.23)
对原数组进行差分,易得:
取
第二行到第三行那里不知道怎么推出来的话,观察每个
因此我们只需要维护两个树状数组,一个维护
注意:此处树状数组维护的仍然是前缀和,
核心代码:
1 |
复制 |