一、树状数组的用处
树状树组是将一个线性数组保存为“树状”,当修改某点的值、求某个区间的和的时候能够有效的减少时间复杂度。当数组长度为N,实时对数组进行M次修改或求和,最坏的情况下复杂度是O(M*N)。
二、树状数组的建立
假设输入数组为
vector<int> nums
将其转化为树状数组的本质在于将数组的原先顺序打乱后,经过特殊的求和方法,组合成新的数组,代码如下。关键点在于k+=k&-k,这是一个利用二进制码的特点完成树状数组下标的选取。
1 size = nums.size(); 2 bitTree = vector<int>(size+1,0); 3 for (int i =1;i<=size;i++) 4 { 5 int k=i; 6 while (k<size+1) 7 { 8 bitTree[k] += nums[i-1]; 9 k += k & -k; 10 } 11 }
树状数组建立的原理:
如下图,来源自百度图片,c数组是树状数组,a数组是原先的线性数组,可以看见树状数组的元素是a数组的元素或者元素之和,其中c[8]+c[9]即是线性数组的a的总和。对于树状数组而言,其建立顺序(按下标排列)是(循环1)c[1]+=a[0];c[2]+=a[0];c[4]+=a[0];c[8]+=a[0];
(循环2)c[2]+=a[1];c[4]+=a[1];c[8]+=a[1];
(循环3)c[3]+=a[2];c[4]+=a[2];c[8]+=a[2];
(循环4)c[4]+=a[3];c[8]+=a[3];
(循环5)c[5]+=a[4];c[8]+=a[4];
(循环6)c[6]+=a[5];c[8]+=a[5];
(循环7)c[7]+=a[6];c[8]+=a[6];
(循环8)c[8]+=a[7];
(循环9)c[9]+=a[8];
可见,第一次循环加的是1,2,4,8;第二次是2,4,8;第三次是3,4,8等等。其规律是第i循环中,c[k]与a[i-1]相加,而k与i的关系是k是i二进制数保留最高位1后相加的结果,比如i=2=0010,其二进制数保留最高位1后是0010,故第1个k=0010+0010=0100=4,再对k进行处理,得第二个k=8,直至k>size+1。又比如i=3=0010,其二进制数保留最高位1后是0010,第1个k=0010+0010=0100,第2个k=0100+0100=8。之所以采用这种方法因为树状数组采用了二分的思想,比如c[8]会等于a[0]~a[3]与a[4]~a[7]两部分的和。
故建立数组的关键在于求i二进制数保留最高位1后相加的结果,其方法是:令k=i,k+=k&-k,即可求得结果。
三、树状数组的更新和部分求和
更新数组:如果此时原数组中的一个元素被改变,那么树状数组中许多值需要被更新,这因为树状数组中的元素之间存在可能的联系,这种联系与树状数组下标值相关。因此更新树状数组并不是单纯的单个值替换,代码如下:
1 void update(int i, int val) { 2 int ret = val-nums[i]; 3 int k=i+1; 4 while (k<size+1) 5 { 6 bitTree[k]+=ret; 7 k+=k&-k; 8 } 9 nums[i] = val; 10 }
数组部分求和:分别先求0到i下标的和,再求0到j+1下标的和,它们之间的差即是下标i到下标j的和,数组部分求和代码如下:
1 int sumRange(int i, int j) { 2 int result1 = 0,result2 = 0; 3 int k=i; 4 while (k) 5 { 6 result1+=sum[k]; 7 k-=k&-k; 8 } 9 k=j+1; 10 while (k) 11 { 12 result2+=sum[k]; 13 k -= k&-k; 14 } 15 return result2-result1; 16 }