当前位置:   article > 正文

关于树状数组

关于树状数组

树状数组,是一个支持在O(log n)时间内,单点修改、区间查询的数据结构,下面这个就是一个树状数组。

看到log n,很容易猜到树状数组利用了二分的思想。c数组的每一项,表示了在这一项的“管辖范围”(图中实线所连接的点)之内的元素之和;而当要查询比如区间[1, 7]时,可以将它拆分成c[4]+c[6]+c[7]。
那么如何确定一个数的管辖范围,又如何拆分呢?不妨看一下对 i 的二进制而言,c[i]的管辖范围有什么特点。

1 (0001): 管辖a[1]
2 (0010): 管辖a[1]~a[2]
3 (0011): 管辖a[3]
4 (0100): 管辖a[1]~a[4]
5 (0101): 管辖a[5]
6 (0110): 管辖a[5]~a[6]


不难发现,如果 i 二进制的末尾有x个0,那么c[i]管辖2^x个数;而且这些数恰好是c[i-2^x+1]~c[i]。下面的问题就是求出2^x了,这个函数一般称作lowbit:

许多其他博客都有对这个的讲解,这里就不再多说了。

  1. inline int lowbit(int i) {
  2. return i & (-i);
  3. }

现在树状数组的定义已经出来了,需要解决的是修改与查询;不难发现,当更新a[i]时,需要更新一切管辖着a[i]的c[j]。当要求出直接管辖a[i]的 j 的时候,上面的lowbit函数就派上用场了: j = i + lowbit(i)。

至于为什么,暂时还没有找到一个清晰而且容易理解的证明……不过举几个例子很容易验证。

把这个公式一直往上代,直到超出n为止,这样得到的每一个点都是管辖着a[i]的,那么修改代码写出来如下:
  1. void modify(int x, int add) {
  2. do c[x] += add;
  3. while ((x += lowbit(x)) <= n);
  4. }

修改代码是从上往下组合,相应地,查询代码就是从上往下拆分。下面的q()查询的是[1, r],而q_range()查询的是[l, r]。
  1. int q(int r) {
  2. int sum = 0;
  3. do sum += c[r];
  4. while (r -= lowbit(r));
  5. }
  6. inline int q_range(int l, int r) {
  7. return q(r) - q(l - 1);
  8. }

自然,树状数组除了单纯作为一个常数比线段树小的单点修改、区间查询的数据结构以外,还有其他的应用,比如求逆序对一类的问题。逆序对除了双重循环O(n^2)的暴力以外,还有一种“另类”(O(nm))的暴力方法:
利用桶排的思想,每次放入一个数,统计比它大的数的个数;其中m是桶的大小。

写出这个方法的代码以后,发现需要不断地修改一个点与扫描区间。单点修改恰好是树状数组的特长,那么只需要试图把扫描区间变化成区间求和,就可以将时间复杂度减为O(nlog n)。可以注意到,每放入一个数,相当于后来来到的比它小的数的逆序对数都需要增加1;那么不妨设置一个数组a,每插入一个数就在对应的地方加上1,并将逆序对数加上从它(不包括)到末尾的区间和。
当数字跨越很大的时候,这是否又太占空间了呢?因此可以使用离散化思想:只需要记录一个元素在原来数组中是第几大就可以了,代码如下:(其中a[i]是a数组中第h[i]大的数)


  1. struct cmp {
  2. bool operator()(int x, int y) {
  3. return a[x] > a[y];
  4. }
  5. }
  6. int main() {
  7. //读入数据到a数组中
  8. for (int i = 0; i < n; ++i)
  9. h[i] = i;
  10. sort(h, h + n, cmp());
  11. //继续下面的操作
  12. }

(本思路来自于https://www.luogu.org/blog/34238/solution-p1908,那个上面有图)

逆序对同时可以用归并排序求出;那么树状数组的特殊之处在哪里呢?可以看看这道题:
https://www.luogu.org/problemnew/show/P1637

(求出一个序列中三元上升子序列的长度)

根据乘法原理,枚举三元上升子序列中间的数 a[i] ,然后求出 i 左边有多少比a[i]小的数,乘以 i 右边有多少比a[i]大的数,累加即为三元上升子序列的个数。这其实是把逆序对改为了“顺序对”,只需要把序列倒过来就可以了。

对于归并排序求逆序对数,每次排序完,原本的序列就被破坏了,需要每次调用时复制一份;这样就平白无故地把时间复杂度乘了一个n,成为了O(n^2 log n)。对于树状数组来说,仍然只需要O(nlog n),因为它并不修改原来的数列。而且对于树状数组,只需要把cmp中的>改为<就可以实现颠倒序列的效果了。代码如下(记得离散化的时候需要去重):
  1. #include<iostream>
  2. #include<algorithm>
  3. #include<numeric>
  4. using namespace std;
  5. //l[i]是左侧小于a[i]的数的个数;r[i]是右侧大于a[i]的数的个数
  6. //c是l的树状数组,C是r的树状数组,n是数列长度,其余字母如上
  7. int c[30010], C[30010], a[30010], h[30010], n, l[30010], r[30010], k[30010];
  8. struct cmp { bool operator()(int x, int y) {return a[x] < a[y];}};
  9. void p(int* w, int x) {do ++w[x]; while ((x += (x & (-x))) <= n);}
  10. int q(int* w, int x) { int s = 0; do s += w[x]; while (x -= (x & (-x))); return s; } //树状数组基本操作;p:单点修改 q:区间查询
  11. void unique() { //给重复数字同一编号
  12. int d = k[h[1]] = 1;
  13. for (int i = 2; i <= n; ++i) {
  14. if (a[h[i]] != a[h[i - 1]]) ++d;
  15. //如果不同,编号自增;也就是说,如果相同就会有同一编号
  16. k[h[i]] = d;
  17. }
  18. }
  19. int main() {
  20. cin >> n;
  21. for (int i = 1; i <= n; ++i) cin >> a[i], h[i] = i;
  22. sort(h + 1, h + n + 1, cmp()); unique() //离散化
  23. for (int i = 1; i <= n; ++i)
  24. p(c, k[i]), l[i] = q(c, k[i] - 1); //注意查询左边区间和的时候并不包括自己,因此要用k[i] - 1
  25. for (int i = n; i >= 1; --i)
  26. p(C, k[i]), r[i] = n - i - q(C, k[i]) + 1; //这里要查询的是右边的区间和,可以用总和(也就是n-i+1)减去左边的区间和q(C, k[i])
  27. cout << inner_product(l + 2, l + n, r + 2, 0) << endl;
  28. return 0;
  29. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/227642
推荐阅读
相关标签
  

闽ICP备14008679号