树状数组
为了表述方便,下面所有的数字,都是二进制形式下的。
拆分成特殊区间------C[i]的定义
树状数组通过特定将区间通过一个特殊地规则,将区间拆分成个区间,其中.
而所谓特殊地规则,是这样的,首先将,首先将用二进制表示,然后将最后一个1变成0即获得了左端点.
之后把作为下一个区间的右端点,重复进行,直到整个区间分完了。
举个例子。假设.划分的区间如下
l | r |
---|---|
100011001000 | 100011001001 |
100011000000 | 100011001000 |
100010000000 | 100011000000 |
100000000000 | 100010000000 |
000000000000 | 100000000000 |
我们将数二进制表示形式下最后一个1的位权(即那个1及其后面的一连串的0表示的数)定义为.那么上面拆分成的特殊区间拆分规则就可以用表示了。
由位运算的知识不难推导出.
可以看到,区间的右端点每次变化的时候,都是去掉一个1.因此,拆分成的子区间数就是i二进制表示下1的个数。假设含有个1,而个1表示的最小数是,即,故,因此.
而树状数组的的定义就是区间拆分出来的第一个区间的部分和。
假设所有的C[i]都已知,那么,对于求前缀和,我们只需要对个进行求和即可。单次前缀和查询复杂度.
C[i]的求取
现在有个问题,如何求?显然直接枚举一个个元素求和是低效的、不明智的。
我们可以充分利用已经前面已经求得的的值。例如表示的区间是.我们将前面两者相同的前缀10001100简记为A.我们可以拆分成:
l | r |
---|---|
A0111 | A1000 |
A0110 | A0111 |
A0100 | A0110 |
A0000 | A0100 |
其中表格第一行所表示的区间只有一个元素,即.剩下的每一行,不难发现表示的区间恰好是.表格的构造也不难,第一行只有一个元素,所以第一行的是.之后每一行的都是上一行的,而这一行的就是.
事实上,也不能说是恰好。因为这是特意要这样构造的。这样拆分,可以充分利用已经求出的并且保证拆分出的不超过的二进制表示下0的个数。表示的区间必然是
的形式。右端点最后有个0,k可以取0.那么就可以将求变成求和个已经求出来的的和的问题.显然是.总体C数组的求取是.
为什么说是树状数组呢?去百度百科里看一下图就知道了。就是把表示的区间往求取所要用到各个连一条边,并且往连一条边,就变成一棵树了。最后就变成树一样的形式了。而的高度,显然取决于的二进制表示下的个数(因为每一层都去掉最后一个1),即至多。因此最高点的高度是。
单点修改a[i]快速更新C数组
到现在为止,我们通过把区间分解成一个个特殊的区间,并且用一个数组C记录这一个个小区间。如果仅仅是求前缀和,那么现在这种做法没有任何优势。因为它多维护了很多不必要的部分区间和。预处理时间复杂度比前缀和差,单次查询前缀和比前缀和差。
但是,我们的确维护了更多的信息,只是这些多维护的信息暂时还没有得到充分的利用,所以显得多余。
当我们就行元素的单点修改的时候,树状数组多维护的部分区间和信息就开始显示出优势了。对于朴素的数组前缀和,当我们修改一个元素只是,所有都要更新。而对于树状数组而言,由于只是维护了部分区间和,所以修改之时,当且仅当所管辖的区间包含之时才需要更新C[j].
那么更新时,如何找到需要更新的所有呢?
只需要解决一个问题即可。刚才是我们求时,是直接找到了直接影响它的所有。即由父亲直接找到了所有的儿子及儿子.那么如何由某一个儿子找到父亲呢?
如果是,那么父亲显然是;如果是c[j],观察上面的例子,不难发现是父亲中的,而通过之前构造出的过程也不难验证这个猜想正确。
因此,更新的时候,只需要一直往上寻找父亲并更新父亲的C值,直到发现到顶了。由于每次都是加自己的lowbit往上走,顶再往上走就超过n了。
BTW,其实C[i]的预处理求取也可以直接通过更新操作来求取,a视作全0数组,那么C数组不论是哪个区间的部分和,和都是0.然后将逐一更新即可。复杂度是一样的。
小结
树状数组C,通过线性的空间复杂度维护一类特殊区间()的部分和信息,以对数的时间复杂度实现了单次自顶向下查询原数组a前缀和的操作,以对数的时间复杂度实现了单次自底向上的修改a[i]并更新C数组的操作。
Code
树状数组的代码非常简单,简直令人发指。相对于线段树简单很多,而且常数比线段树小不少。当然,线段树可能多支持一些操作。
inline int lowbit(int x) {return x&(-x);}
inline int lft(int x) {return x&(-x)^x;}
inline int upf(int x) {return x+lowbit(x);}
// 原始数组a下标从1开始
// 树状数组 c[i]是初始数组a的部分区间和
// i所掌管的区间是(lft(i),i]
ll a[maxn];
int n;
ll c[maxn];
void add(int i,ll val) {
while (i <= n) c[i] += val, i = upf(i);
}
ll sum(int i) {
ll ans = 0;
while (i) ans += c[i],i = lft(i);
return ans;
}
一些发散的思考
C[i]的更广泛的定义
前面我们将C[i]定义成了特定区间的区间部分和。但是C[i]不一定要是定义成区间的部分和,只要是满足结合律的运算的函数应该都可以,例如定义成区间内所有元素的异或值、最值之类的。
但是考虑到单点修改a[i]的时候,我们是把包含的进行更新的。
假设直接指定了修改操作是"+val"(这里的+不是普通意义的加法,只是代表一种运算)。理论上更新后的的值应该是,而我们修改操作的算法是,即计算的是.理论上的值要和我们实际计算出来的值恒相等就必须使得我们定义的+运算必须满足交换性。
如果单点修改操作不是直接给出如何修改,而是给出修改后的结果,即直接给出修改后的,则定义的+运算则还需要加上存在逆元(逆操作)这一条。即要变成,如果可交换并且有逆元,那么就可以计算出目标值。即可以消去原本的影响。例如异或可以,但是求区间最大值却不可(因为无逆元)。
当然,如果不要求保持单次修改操作的时间复杂度是,那么只需要满足结合性即可。显然,我们需要修改我们的修改操作的算法。我们依旧是自底向上更新。对于的更新,我们不能利用原本的的信息直接计算,而是用的所有儿子按照其所代表的区间顺序(如果不满足交换性)重新计算一番。另外,对于区间的问题,不能变成与的差的问题,因为无法消除的影响。于是只能使用逐步分解为儿子的一个个区间后进行的合并的方法。单点修改复杂度将变成,并且代码没那么优美了.因此,最值问题也可以使用树状数组做,只是复杂度多乘以了一个对数。
BTW,事实上,线段树之所以能处理最值问题就是因为线段树采取的是重新用儿子计算。只是,线段树的一个节点的儿子就只有两个!
与差分的结合
小结中已经可以看到,树状数组支持单点修改,前缀和查询(之后容易推出区间和)查询。
那么区间集体增加一个数,查询单点值呢?这个引入差分数组即可。区间集体加将等价于差分数组的两个点的单点修改。查询单点值将等价于查询差分数组的前缀和。
如果是区间集体加,查询区间和呢?依旧引进差分数组b,则前缀和.所以只需要引入数组.如此,区间集体加将是变成c数组的2次单点修改,区间求和将变成c数组的两次前缀和查询。再次变成树状数组的题目。