树状数组的那啥啥啥

emmmmm, 在我们学习树状数组之前, 我们应该知道lowbit(n)运算, lowbit(n)定义为非负整数n在二进制下“最低位的1及后面所有的0”构成的数值, 例如n = 10的二进制表示为\((1010)_2\),则\(lowbit(n) = 2 = (10)_2\), 显然可知

\[lowbit(n) = n \And (\sim n +1) = n \And (-n)\]

所以我们把一个序列分成\(log(x)\)小区间,

    while(x > 0) {
        printf("[%d %d]\n", x - (x & -x) + 1, x);
        x -= x & -x;
    }

树状数组就是基于上述思想的数据结构, 其基本用途是维护序列的前缀和。对于给定的序列a, 我们建立一个数组c, 其中c[x]保存序列a的区间[x - lowbit(x) + 1, x]中所有数的和, 及\(\sum^x_{x - lowbit(x) + 1}a[i]\)

那我们就看树状数组最基本的操作吧

1.单点修改, 区间查询

题目描述

如题,已知一个数列,你需要进行下面两种操作:

1.将某一个数加上x

2.求出某区间每一个数的和

输入格式

第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。

第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。

接下来M行每行包含3个整数,表示一个操作,具体如下:

操作1: 格式:1 x k 含义:将第x个数加上k

操作2: 格式:2 x y 含义:输出区间[x,y]内每个数的和

输出格式

输出包含若干行整数,即为所有操作2的结果。


对于查询1~x的前缀和, 我们按照刚才提出的方法, 应该求出x二进制表示中每个等于1的位, 把[1, x]分成\(log(n)\)个小区间, 而每个小区间和都已经保存在数组c中。所以把上边的代码稍加改写即可在\(log(n)\)的时间内查询前缀和:

inline int ask(int x) {
    int ans = 0;
    for(; x > 0; x -= x & -x) ans += c[x];
    return ans;
} 

当然, 若要查询区间[l, r]中所有数的和, 只需要计算\(ask(r)-ask(l - 1)\)

对于单点修改, 一个数的改变会影响c[x]即其所有祖先节点保存的"区间和"包括a[x],而任意一个节点的祖先最多有\(log(n)\)个, 我们逐一对它们的c值更新即可。

inline void add(int x, int k) {
    for(; x <= n; x += x & -x) c[x] += k;
}

在执行所有操作之前, 我们需要对树状数组进行初始化--针对原始序列a构造一个树状数组

为了简便, 我们一边初始的方法是, 每读入一个a[i], 执行add(i, a[i])的操作, 时间复杂度是\(O(nlogn)\), 通常这种方法已经足够。
还有一种更高效的方法, 用前缀和的方式直接更新c[x], 时间复杂度为\(O(n)\), 不过用这种方法需要多开一个前缀和数组

    for(int i = 1; i <= n; ++i) {
        read(a[i]);
        add(i, a[i]);
    }
    for(int i = 1; i <= n; ++i) {
        read(a[i]);
        sum[i] = sum[i - 1] + a[i];
    }
    for(int i = 1; i <= n; ++i) {
        c[i] = sum[i] - sum[i - (i & -i)];
    }

2.区间查询, 单点修改

题目描述

如题,已知一个数列,你需要进行下面两种操作:

1.将某区间每一个数数加上x

2.求出某一个数的值

输入格式

第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。

第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。

接下来M行每行包含2或4个整数,表示一个操作,具体如下:

操作1: 格式:1 x y k 含义:将区间[x,y]内每个数加上k

操作2: 格式:2 x 含义:输出第x个数的值

输出格式

输出包含若干行整数,即为所有操作2的结果。

猜你喜欢

转载自www.cnblogs.com/AK-ls/p/11607508.html