树状数组,听起来是一个多么平凡的数据结构名称,顾名思义就是开一个树一样的数组。但是经过这几天的学习我发现树状数组——普通而奇妙。
引入:
现在有这么一个问题:给你一个数列a,让你求出某区间(i, j)内元素的和,你会怎么做??如果你没学过树状数组或者线段树这样的数据结构的话,你可能会天真地回答当然是在那多区间扫一遍,把元素累加到一个变量(如:sum。。叫法当然随心所欲)里,不就妥妥地搞定了吗~
这样当然没问题,但是!如果随时要改变元素的值,随时向不同区间询问区间和,而且区间的长度很大很大呢??那种扫一遍,逐个改变,逐个累加的算法岂不是很慢很慢。所以,树状数组这个神奇的就够就华丽丽的出现了。
大家观察上面的图片,有没有发现什么?其实这就是树状数组的模型。如果你还没发现其中的奥秘——
c[1] = a[1];
c[2] = a[1] + a[2] ;
c[3] = c[3];
c[4] = a[1] + a[2] + a[3] + a[4]
c[5] = a[5];
c[6] = a[5] + a[6];
c[7] = a[7];
c[8] = a[1] + a[2] + a[3] + a[4] + a[5] + a[6] + a[7] + a[8];
. . . . . .
大家应该可以发现——这里出现了一个神奇而有趣的性质:
对于每一个节点x,它管辖的范围就是它前面的2^k(k代表x在二进制下末尾0的个数)个元素.
就可以得出:c[i] = a[i - 2 ^k + 1] + ... + a[i](i 从1开始算)。注意!c[i]储存的是[a - 2^k, a) 这段区间的 元素和!
这样就达到了我们区间求和的目的, 而且时间的复杂度由最朴素的O(n) 降低到了O(logn)。则我们称这个c数组叫做树状数组。
下面问题来了,给出i,如何求得2 ^ k呢?
最奇妙的地方来了:2 ^ k = x & (x ^ (x - 1)) 或 = x & (-x);
举例:当i = 6 时, 6在二进制下 = (0110);
(6 - 1) = 5 在二进制下 = (0101);
(0110) ^ (0101) = (1100);
(0011) & (0110) = (0010);
所以,i = 6时,其管辖范围2 ^ k = 2 ^ 1 = 2;
int Lowbit(int x)
{
return (x & (x ^ (x - 1)));
}
接下来,我们该考虑改变元素的值和求和的问题了——
1)求和——
step1 : int sum, sum = 0; - > step2;
step2 : 假如 i <= 0, 直接返回sum; 否则 sum += c[i]; - > step3;
step3 : i -= Lowbit(i); - > step2;
可以看出,这个算法就是将这一个个区间的和全部加起来,为什么是效率是log(n)的呢?以下给出证明:n = n – lowbit(n)这一步实际上等价于将n的二进制的最后一个1减去。而n的二进制里最多有log(n)个1,所以查询效率是log(n)的。
int Sum(int end)
{
int sum = 0;
for (i = end; i > 0; i -= Lowbit(end))
sum += c[i];
return sum;
}
2)改变元素——
修改一个节点,必须修改其所有祖先,最坏情况下为修改第一个元素,最多有log(n)的祖先。所以修改算法如下(给某个结点i加上x)。
step1:当i > n时,算法结束,否则转第二步。
step2: c[i] = c[i] + x, i = i + Lowbit(i)转第一步。
i = i + Lowbit( i ) 这个过程实际上也只是一个把末尾1补为0的过程。
对于数组求和来说树状数组简直太快了!
void change(int pos, int x)
{
for (int i = pos; i <= n; i += Lowbit(pos))
c[pos] += x;
}