树状数组

Posted watchphone

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了树状数组相关的知识,希望对你有一定的参考价值。

树状数组

一、长什么样?


假设有一数组a,数组b为a的前缀和数组,即b[i] = a[i] + a[i-1] + ... + a[1],树状数组c为a的(部分)前缀和数组,即c[i] = a[i] + a[i-1] + ... + a[i+1-lowbit(i)],也即c[i]为lowbit(i)个数组a的元素的和。

lowbit(i): 把i以二进制展开,只留下最低位的1及其后的0,其余位清零后所得的数值。注意i & -i可以直接算出这个值。

上述方式所得的数组c中每个元素对应的数组a元素和的个数,由下图可以看得很清晰,注意第一个元素从下标1开始。

技术图片

进一步,求b[i]可以用x个数组c中的元素相加,其中x = 数字i的二进制展开中bit1的个数,具体由哪几个元素相加看图容易得到,下标的计算可以参考具体的代码。动态更新a[i]时,需要更新b[j],其中b[j] = a[j] + ... + a[i] + ... ,即含a[i]的数组b中的元素都需要被更新。

若用普通数组动态维护前缀和,复杂度为O(n),若用树状数组,由二进制展开的特性易知复杂度则为O(logn),看图也易得。

二、怎么维护?

1.单点修改 区间查询(查询区间和)

  • 初始化树状数组c为0(第一个元素下标为1, 下标为0的元素也要初始化为0)

  • 原数组a的第x个元素加v

    void add(int x, int v) { 
      while (x <= n) {
          c[x] += v; //c为a对应的树状数组
          x += lowbit(x); 
      }
    }
  • 求原数组a中第一个至第x个元素的和

    int sum(int x) {
      int t = 0;
      while (x) {
          t += c[x];
          x -= lowbit(x);
      }
      return t;
    }
  • 区间[x1, x2]和查询

    int query(int x1, int x2) {return sum(x2) - sum(x1 - 1);}
2.区间修改,单点查询(对区间内所有值加上某一值)

思路:差分,设差分数组p,则p[i] = a[i] - a[i-1]=>a[i] = p[i] + p[i-1] + ... + p[1]。维护差分数组p的树状数组即可。
add() sum()的实现同单点修改,区间查询。

  • 初始化树状数组c为0

  • 初始化赋值时要注意维护的是差分数组p对应的树状数组c。例如:在原数组a下标为x处初始化为v,对应于在数组p中给p[x]初始化为v-p[x-1]。(p的初值置为0)

    add(x, v - sum(x-1)); //按下标连续初始化时不用调用sum()
  • 原数组a中下标在[x1, x2]范围的值均加上某值v,对应于在数组p中给p[x1]加v,给p[x2+1]减v

    add(x1, v), add(x2 + 1, -v);
  • 查询原数组a中下标为x的值,对应于求p[x] + p[x-1] + ... + p[1],即求p的某前缀和

    sum(x);
3.区间修改,区间查询

由区间修改,单点查询的基础上,优化一下区间查询的效率即可。首先推一下公式,随便截了个图,该图的描述方法和之前的有些不同。

di为差分数组的第i个元素,an为原数组第n个元素。公式表示原数组的前缀和,等价变换可得,原数组的前缀和可以转化为差分数组的前缀和以及di*i的前缀和。故得一思路:通过树状数组维护差分数组的前缀和,再通过公式计算出原数组的前缀和。

技术图片

  • add() sum()稍作修改:

    void add(int x, int v, int *a) {
      while (x <= n) {
        a[x] += v;
        x += lowbit(x);
      }
    }
    
    int sum(int x, int *a) {
      int ret = 0;
      while (x) {
        ret += a[x];
        x -= lowbit(x);
      }
      return ret;
    }
  • 初始化树状数组为0

  • 初始化赋值:维护差分数组的同时维护数组array,其中array[i] = di * i,具体初始化的方法与区间修改,单点查询的情况类似。设在原数组a下标为x处初始化为v。

    int t = v - sum(x-1); //按下标连续初始化时不用调用sum()
    add(x, t, 差分数组的指针);
    add(x, t * x, array的指针);
  • 原数组中下标在[x1, x2]范围的值均加上某值v

    add(x1, v, 差分数组的指针), add(x2 + 1, -v, array的指针); 
    add(x1, v * x1, 差分数组的指针), add(x2 + 1, -v * (x2 + 1), array的指针);
  • 查询原数组[x1, x2]区间和为

    ( (x2 + 1) * sum(x2, 差分数组的指针) - sum(x2, array的指针) )
          - ( x1 * sum(x1 - 1, 差分数组的指针) - sum(x1 - 1, array的指针) );

三、模版题各一道。。


1单点修改,区间查询 洛谷P3374

#include <cstdio>
const int N = 500000 + 20;

int n, m, c[N];
inline int read() {
    int x = 0, f = 1; char ch = getchar();
    while ('0' > ch || ch > '9') {if (ch == '-') f = -1; ch = getchar();}
    while ('0' <= ch && ch <= '9') {x = 10 * x + ch - '0'; ch = getchar();}
    return x * f;
}

inline int lowbit(int x) {return x & -x;}

void add(int x, int v) {
    while (x <= n) {
        c[x] += v;
        x += lowbit(x);
    }
}

int sum(int x) {
    int t = 0;
    while (x) {
        t += c[x];
        x -= lowbit(x);
    }
    return t;
}

int main() {
    n = read(), m = read();
    for (int i = 1; i <= n; i++) add(i, read());
    while (m--) {
        int cmd = read(), k1 = read(), k2 = read();
        if (cmd == 1) add(k1, k2);
        else printf("%d
", sum(k2) - sum(k1 - 1));
    }
    return 0;
}

2区间修改,单点查询 洛谷P3368

#include <cstdio>
const int N = 500000 + 20;
int n, m, c[N];

inline int read() {
    int x = 0, f = 1; char ch = getchar();
    while ('0' > ch || ch > '9') {if (ch == '-') f = -1; ch = getchar();}
    while ('0' <= ch && ch <= '9') {x = 10 * x + ch - '0'; ch = getchar();}
    return x * f;
}

inline int lowbit(int x) {return x & -x;}

void add(int x, int v) {
    while (x <= n) {
        c[x] += v;
        x += lowbit(x);
    }
}

int sum(int x) {
    int t = 0;
    while (x) {
        t += c[x];
        x -= lowbit(x);
    }
    return t;
}

int main() {
    n = read(), m = read();
    int last = 0, t;
    for (int i = 1; i <= n; i++) {
        t = read();
        add(i, t - last);
        last = t;
    }
    while (m--) {
        int cmd = read();
        if (cmd == 1) {
            int x = read(), y = read(), k = read();
            add(x, k), add(y + 1, -k);
        } else {
            int x = read();
            printf("%d
", sum(x));
        }
    }
    return 0;
}

3区间修改,区间查询 poj3468

#include <cstdio>
#include <cstring>
typedef long long ll;
const int N = 100000 + 10;
ll n, q;
ll num1[N], num2[N];

inline ll lowbit(ll x) {return x & (-x);}
void add(ll x, ll v, ll *a) {
    while (x <= n) {
        a[x] += v;
        x += lowbit(x);
    }
}

ll sum(ll x, ll *a) {
    ll ret = 0;
    while (x) {
        ret += a[x];
        x -= lowbit(x);
    }
    return ret;
}

inline ll query(ll a, ll b) {
    return (b + 1) * sum(b, num1) - sum(b, num2) 
        - (a * sum(a - 1, num1) - sum(a - 1, num2));
}

int main() {
    scanf("%lld%lld", &n, &q);
    ll last = 0, t;
    for (int i = 1; i <= n; i++) {
        scanf("%lld", &t);
        add(i, t - last, num1);
        add(i, (t - last) * i, num2); //一开始写成num1。。
        last = t;
    }
    char line[30];
    ll a, b, c;
    getchar(); //别忘了。。
    while (q--) {
        gets(line);
        if (line[0] == 'Q') {
            sscanf(line + 1, "%lld%lld", &a, &b);
            printf("%lld
", query(a, b));
        } else {
            sscanf(line + 1, "%lld%lld%lld", &a, &b, &c);
            add(a, c, num1);
            add(b + 1, -c, num1);
            add(a, c * a, num2);
            add(b + 1, -c * (b + 1), num2);
        }
    }
    return 0;
}

`

以上是关于树状数组的主要内容,如果未能解决你的问题,请参考以下文章

数据结构之树状数组从零认识树状数组

树状数组和线段树有啥区别?

树状数组

树状数组

树状数组

如何利用树状数组修改一个区间?