『数据结构』树状数组
树状数组的问题模型:
现在有一个这样的问题:
有一个数组\(a\),下标从\(0\)到\(n-1\),现在你要进行\(w\)次修改,\(q\)次查询。
修改是修改数组中某一个元素的值;
查询是查询数组中任意一个区间的和,\(w+q<500000\)。
这个问题很普遍,首先分析下朴素做法的时间复杂度,
修改是\(O(1)\)的时间复杂度,
而查询就要\(O(n^2)\)的复杂度,总体时间复杂度为\(O(q*q*n*n)\);
你或许会想到用前缀和来优化这个查询,
我们再来分析下,查询的话是\(O(1)\)的复杂度,
但是修改的时候修改一个点,那么在之后的所有前缀和都要更新,
所以修改的时间复杂度是\(O(n^2)\),总体时间复杂度还是\(O(q*q*n*n)\)。
可以发现,两种做法中,要么查询是\(O(1)\),修改是\(O(n^2)\);
要么修改是\(O(1)\),查询是\(O(n^2)\)。
有没有一种做法可以降低时间复杂度呢?树状数组。
我们先来了解下\(lowbit\)这个函数,
你也先不要问这个函数到底在树状数组中有什么用;
\(lowbit\)这个函数的功能就是求某一个数的二进制表示中最低的一位1,
\(for\) \(example\),\(x=6\),它的二进制为\(110\),
那么\(lowbit(x)\)就返回\(2\),因为最后一位\(1\)表示\(2\)。
我们怎么求\(lowbit\)呢?
求负数的补码的简便方法:
先把这个数的二进制写出来,然后从右向左找到第一个\(1\),这个\(1\)不要动和这个\(1\)右边的二进制不变,左边的二进制依次取反,这样就求出的一个数的补码,说这个方法主要是让我们理解一个负数的补码在二进制上的特征,然后我们把这个负数对应的正数与该负数与运算一下,由于这个\(1\)的左边的二进制与正数的原码对应的部分是相反的,所以相与一定都为\(0\),由于这个\(1\)和这个\(1\)右边的二进制都是不变的,因此,相与后还是原来的样子,
所以,这个得出的结果就是\(lowbit(x)\)的结果。
lowbit函数:
int lowbit(x)
{
return x & -x;
}
二进制的视角:一个数\(n\),假设\(n=6\),它的二进制为\(110\),我们把它表示成累加的形式\(110=100+10\),这样是可以的,那么我们要求前\(6(110)\)项的和可以这样求:
\(∑i=16=(a[1]+a[2]+a[3]+a[4])+(a[5]+a[6])\)
注意括号中的元素个数,
是不是\(4(100)\)个加\(2(10)\)个,
和\(110=100+10\)是不是很像,
\(10\)就是\(lowbit(110)\)的结果,\(100\)是\(lowbit(100)\)的结果。
求和的时候我们总是把\(∑ni=1\)和\(∑i=1n\)拆分成这样的几段区间和来计算,
区间的起点和长度就是根据\(n\)的二进制来的,
二进制怎么拆的,你就怎么拆分,而拆分二进制就要用到上面说的\(lowbit\)函数了。
这里也可以顺理成章得给出\(c\)数组的表示了。
\(c[i]\)表示从第i个元素向前数\(lowbit(i)\)个元素,这一段的和,这就是上面说的区间和,只不过这个区间是靠右端点的;你可能又想问,不是说区间是靠右端点的吗,是后缀和啊,那中间的这些区间怎么定义?其实递归定义就好了,比如说:
\(∑6i=1=(a[1]+a[2]+a[3]+a[4]+(a[5]+a[6])=\)
\(∑6i=1=(a[1]+a[2]+a[3]+a[4])+c[6]\)
\(∑i=16=(a[1]+a[2]+a[3]+a[4])+(a[5]+a[6])=\)
\(∑i=16=(a[1]+a[2]+a[3]+a[4])+c[6];\)
你可以把\(c[6]\)去掉,就变成了
\(∑4i=1=(a[1]+a[2]+a[3]+a[4])\)
\(∑i=14=(a[1]+a[2]+a[3]+a[4])\)
这个区间就靠右端点了,
\(∑4i=1=c[4]=c[6-lowbit(6)]\)
\(∑i=14=c[4]=c[6-lowbit(6)]\)。
设计一种数据结构,需要的操作无非就是更改和查询,
这里只讨论查询和修改操作具体是怎么实现的;
查询
这里说的查询是查询任一区间的和,由于区间和具有可加减性,所以转化为求前缀和;
查询前缀和就是把大区间分成几段长度不等的小区间,然后再求和。
区间的个数为\(O(logn*logn)\),
所以查询的时间复杂度为\(O(logn*logn)\)。
修改
修改某一位置上的元素的时间复杂度为\(O(1)\),
但是要更新\(c\)数组,不然查询的时间复杂度就会变高。
更新的方法就要提一下树状数组的性质了和树状数组那张经典的图片了。
图片中已经把\(c\)数组的后缀和这个含义已经表达得很清楚了。
这个时候你再把查询操作对应到这张图上,
然后根据二进制来操作,
就可以很直白地理解上面所说的查询操作了!
树状数组的代码实现
对某个元素进行加法操作:
void update(int x) {
while(x<=n) {
c[i]+=x;
x+=lowbit(x);
}
}
查询前缀和:
int sum(int x)
{
int res=0;
while (x>0)
{
res+=c[x];
x-=lowbit(x);
}
return res;
}
树状数组练习题: