【ACM之路】8.树状数组

1.先验知识

适用于“单点更新,区间求和”或者“区间更新,单点求和”。

假设有一个数组A,有两种操作,1是更新某一项的值,2是查询(l, r)区间的值,这是“单点更新,区间求和”。我们先看下面的图片。

从图片可以看到,

C[1] = A[1]

C[2] = C[1] + A[2]  = A[1] + A[2]

C[3] = A[3]

C[4] = C[2] + C[3] + A[4] =  A[1] + A[2] +  A[3] + A[4]

.......

有什么规律吗?

可以看到,(C[i]代表的是A[i - 2^k + 1] + A[i - 2^k + 2] ..... + A[i]的值),这里的k代表i二进制中"最后一个1后面0的个数“,我们称2^k为lowbit(i)

什么是lowbit(i)呢。lowbit(i)表示的是“最后一个1加”,如果有0加所有的0,如lowbit(5), 5的二进制是101,最后1位是1,所以lowbit(5)等于1;再如lowbit(6),二进制是110,所以lowbit(6) = 2.

综上所述,C[i]代表的值就为A[i - lowbit(i) + 1] + A[i - lowbit(i) + 2] + .... + A[i],如C[4],4的lowbit为100,也即4本身,所以C[4] = A[4 - 4 + 1] + A[4 - 4 + 2] + ... + A[4] = A[1] + A[2] + A[3] + A[4].

然后我们再给出一种求lowbit(i)的建议代码:(这是利用计算机补码的性质实现的)

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

2. 单点更新,区间求和

2.1单点更新:

由下面的图可以看到,如果更新A数组第3个点,也即A[3]的值,如加5,则C[4] ,C[8]也要加5,那加到什么时候停止呢?假设A数组有n个值,则加到超过n时停止。

我们给出单点更新的代码:(也即更新x点的数值,加上y,则每个包括x点值的C[i]都要更新)

update(int x, int y) {
    for (int i = x; i <= n; i += lowbit(i)) {
        C[i] += y;
    }
}

为了方便理解,给出了下面的图:A[3]更新了,则C[3],C[4],C[8]都要更新。 

星号代表这个点需要更新

2.2区间求和

我们先想想求A[1] + A[2] + ... + A[n]的情况。如求1~8的和,则C[8]一步就给出了答案,如果求1~6的和,则只求C[4] + C[6]即可。我们先给出求1~x,这x个数的和的代码:

LL query(int x) {
    LL ans =  0;
    for (int i = x; i > 0; i -= lowbit(i)) {
        ans += C[i];
    }
    return ans;
}

如求1~6的和,则ans += C[6] , 6 - lowbit(6) = 4. ans += C[4]. 4 - lowbit(4) = 0; i ==0 所以推出,因此返回的就是C[4] + C[6]。很简单吧。

既然已经知道了如何求1~x的值,那么如果想知道(L,R)区间的值,我们查询1~R的值减去查询1~(L - 1)的值,剩下的范围不就是(l, r)的值了吗?用代码表示也即query(R) - query(L - 1).

2.3 总结

我们比较一下计算复杂度,假如有A数组有n个值,m次提问。则暴力计算区间和的时间复杂度为O(n),总T(n) = O(nm)。如果用了树状数组,用了二进制的思想,查询时间很快。相当于是log级别的。T(n) = O(mlogn)

暴力和树状数组的空间复杂度都是O(n).

Note:计算lowbit(i)时i不能为0,应该从1开始,因为如果i  = 0 在update和query时会陷入死循环。(可以自己试一下效果)

2.4 例题

模板题:https://www.luogu.com.cn/problem/P3374(下面有代码参考)

例题:http://poj.org/problem?id=2299(POJ2299,考虑“离散化”->“树状数组”,可以看我的博客:<a href = "#">shuzhuangshuzu </a>)

这里给出模板题的代码,例题代码:

#include <iostream>
#include <cstdio>
using namespace std;
#define ll long long

ll c[500005];
int n, m, x;
int op, a, b;
//求"最后一个1"加后面的0
int lowbit(int x) {
    return x & (-x);
}
//单点更新:x节点的值加y
void update(int x, ll y) {
    for (int i = x; i <= n; i += lowbit(i)) {
        c[i] += y;
    }
}
//求1->x的值.若求(l,r)用query(r)-query(l-1)
int query(int x) {
    ll ans = 0;
    for (int i = x; i > 0; i -= lowbit(i)) {
        ans += c[i];
    }
    return ans;
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) {
        scanf("%d", &x);
        update(i, x);
    }
    while (m--) {
        scanf("%d%d%d", &op, &a, &b);
        if (op == 1) {
            update(a, b);
        } else {
            printf("%d\n", query(b) - query(a - 1));
        }
    }
    return 0;
}

3.区间更新,单点求和

3.1区间更新

区间更新用到了差分数组的思想。设数组A共有n个元素,为A[1], A[2], ...A[n]。令A[0] = 0。

a[1] = A[1] - A[0]

a[2] = A[2] - A[1]

a[3] = A[3] - A[2]

...

a[n] = A[n] - A[n - 1]

如果要更新(L, R)区间的值,如+ 3,则令a[L] += 3。a[R + 1] -= 3

如更新(1, 3)区间的值,则

a[1] = A[1] - A[0] + 3

a[2] = A[2] - A[1]

a[3] = A[3] - A[2]

a[4] = A[4] - A[3] - 3

则区间更新的代码很容易:

//给定l, r, x,(l,r)代表范围。x代表更新的值

a[l] += x;
a[r + 1] -= x;

3.2单点求和

我们容易发现下面的结论:由于A[0] = 0

a[1] = A[1]

a[1] + a[2] = A[2]

a[1] + a[2] + a[3] = A[3]

...

因此,如果要求A[k],也就是求a数组的前k项和,那么这里显然要用到树状数组了,查询快。上面的单点更新是不影响A的计算的,不信的话,试一试,发现很神奇。查询的代码就是query(x)的代码,与“单点更新,区间求和”的查询代码相同。但是注意查询的是小a数组的树状数组,而不是大A数组的树状数组。

3.3 例题

模板题:https://www.luogu.com.cn/problem/P3368

例题代码:

#include <iostream>
#include <cstdio>
using namespace std;
int n, m;
int c[500005];
int lowbit(int x) {
    return x & (-x);
}
void update(int x, int y) {
    for (int i = x; i <= n; i += lowbit(i)) {
        c[i] += y;
    }
}
int query(int x) {
    int ans = 0;
    for (int i = x; i > 0; i -= lowbit(i)) {
        ans += c[i];
    }
    return ans;
}
int main() {
    scanf("%d%d", &n, &m);
    int pre = 0, now = 0;
    for (int i = 1; i <= n; i++) {
        scanf("%d", &now);
        update(i, now - pre);
        pre = now;
    }
    int opt, x, y, k;
    while (m--) {
        scanf("%d", &opt);
        if (opt & 1) {
            scanf("%d%d%d", &x, &y, &k);
            update(x, k);
            update(y + 1, -k);
        } else {
            scanf("%d", &x);
            printf("%d\n", query(x));
        }
    }
    return 0;
}

4.多维树状数组:

假如有一个数组A[n][m],也可以建立一个二维的数组,需要更新一维的update和query,并把for循环转化为二维。

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值