【洛谷】P3374 【模板】树状数组 1

题目地址:

https://www.luogu.com.cn/problem/P3374

题目描述:
如题,已知一个数列,你需要进行下面两种操作:将某一个数加上 x x x,求出某区间每一个数的和。

输入格式:
第一行包含两个正整数 n , m n,m n,m,分别表示该数列数字的个数和操作的总个数。第二行包含 n n n个用空格分隔的整数,其中第 i i i个数字表示数列第 i i i项的初始值。接下来 m m m行每行包含 3 3 3个整数,表示一个操作,具体如下:1 x k含义:将第 x x x个数加上 k k k2 x y含义:输出区间 [ x , y ] [x,y] [x,y]内每个数的和。

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

数据范围:
对于 30 % 30\% 30%的数据, 1 ≤ n ≤ 8 1≤n≤8 1n8 1 ≤ m ≤ 10 1≤m≤10 1m10
对于 70 % 70\% 70%的数据, 1 ≤ n , m ≤ 1 0 4 1\le n,m \le 10^4 1n,m104
对于 100 % 100\% 100%的数据, 1 ≤ n , m ≤ 5 × 1 0 5 1\le n,m \le 5\times 10^5 1n,m5×105

树状数组也叫Fenwick Tree或者Binary Indexed Tree,建树时间 O ( n log ⁡ n ) O(n\log n) O(nlogn)(可以达到 O ( n ) O(n) O(n)),支持 O ( log ⁡ n ) O(\log n) O(logn)查询数组前缀和(于是就支持 O ( log ⁡ n ) O(\log n) O(logn)的范围求和),也支持 O ( log ⁡ n ) O(\log n) O(logn)更新某一数组值。相比于前缀和数组更新数组值需要 O ( n ) O(n) O(n)时间来说,它的更新数组值的效率要高一些。树状数组本质上是个数组,所谓的“建树”实际意思是建数组。

注意:树状数组建树的时候,是从数组的下标 1 1 1开始的, 0 0 0被忽略了。我们先回顾前缀和数组。前缀和数组的缺点在于,由于前缀和数组每个数都保存了原数组当前位置之前所有数字的和,靠后的位置管得太“宽”,导致在更新的时候,如果更新数的位置太靠前,就会有“牵一发而动全身”的效果,后面的数都得改,导致复杂度太高。

树状数组这个数据结构的主要思想就是,让每个数只管辖自己位置附近的数,这样在更新的时候,就能降低复杂度(当然作为代价,建树和查询时间的复杂度增加了)。所以在建一次但查询和更新频繁的场合,树状数组有着不错的性能。

详细说明如下:任何区间 [ 1 , n ] [1,n] [1,n]都可以分为这样的区间的并: [ 1 , 2 ] , [ 2 ∗ 1 + 1 , 2 ∗ 2 ] , [ 2 ∗ 2 + 1 , 2 ∗ 3 ] , . . . , [ 2 k + 1 , n ] [1,2],[2*1+1,2*2],[2*2+1,2*3],...,[2k+1,n] [1,2],[21+1,22],[22+1,23],...,[2k+1,n]也就是: [ 1 , 2 ] , [ 3 , 4 ] , [ 5 , 6 ] , . . . , [ 2 k + 1 , n ] [1,2],[3,4],[5,6],...,[2k+1,n] [1,2],[3,4],[5,6],...,[2k+1,n]其中 k k k满足 2 k + 1 ≤ n ≤ 2 ( k + 1 ) 2k+1\le n\le 2(k+1) 2k+1n2(k+1)由数论的基本知识知道, k = ⌊ n − 1 2 ⌋ k=\lfloor \frac{n-1}{2}\rfloor k=2n1。现在构造这样的树形结构,每个区间的左端点看作叶子节点,每个叶子节点的父亲节点正好是区间右短点,然后叶子节点就存当前数组的数,父亲存整个区间的数的和。接着,对上面的区间两两合并,成为: [ 1 , 2 ∗ 2 ] , [ 2 ∗ 2 + 1 , 2 ∗ 4 ] , [ 2 ∗ 4 + 1 , 2 ∗ 6 ] . . . , [ 2 ∗ ( k − 1 ) + 1 , n ] [1,2*2],[2*2+1,2*4],[2*4+1,2*6]...,[2*(k-1)+1,n] [1,22],[22+1,24],[24+1,26]...,[2(k1)+1,n]也就是: [ 1 , 4 ] , [ 5 , 8 ] , [ 9 , 12 ] , . . . , [ 2 k − 1 , n ] [1,4],[5,8],[9,12],...,[2k-1,n] [1,4],[5,8],[9,12],...,[2k1,n]然后让新区间的右端点“升级”,新接管本范围的原来的其它父亲节点,把自己新接管的父亲节点的数加在自己身上。如此一直操作,直到区间全部合并完。可以看到,每次合并后,区间的长度就翻倍。

上述描述结合具体例子会看得更清楚:
Fenwick Tree比如对于图中的数组 [ 1 , 7 , 3 , 0 , 5 , 8 , 3 , 2 , 6 , 2 , 1 , 1 , 4 , 5 ] [1,7,3,0,5,8,3,2,6,2,1,1,4,5] [1,7,3,0,5,8,3,2,6,2,1,1,4,5],建树过程如下:先把原数组分割为: [ 1 , 7 ] , [ 3 , 0 ] , [ 5 , 8 ] , [ 3 , 2 ] , [ 6 , 2 ] , [ 1 , 1 ] , [ 4 , 5 ] [1,7],[3,0],[5,8],[3,2],[6,2],[1,1],[4,5] [1,7],[3,0],[5,8],[3,2],[6,2],[1,1],[4,5]可以把上述区间想象成一个一个“村”,然后每个村任命村长,把每个村的右端点升为村长,管上本村的数字和: [ 1 , 8 ] , [ 3 , 3 ] , [ 5 , 13 ] , [ 3 , 5 ] , [ 6 , 8 ] , [ 1 , 2 ] , [ 4 , 9 ] [1,\textbf{8}],[3,\textbf{3}],[5,\textbf{13}],[3,\textbf{5}],[6,\textbf{8}],[1,\textbf{2}],[4,\textbf{9}] [1,8],[3,3],[5,13],[3,5],[6,8],[1,2],[4,9]接着把这些“村”合并为“市”: [ 1 , 8 , 3 , 3 ] , [ 5 , 13 , 3 , 5 ] , [ 6 , 8 , 1 , 2 ] , [ 4 , 9 ] [1,8,3,3],[5,13,3,5],[6,8,1,2],[4,9] [1,8,3,3],[5,13,3,5],[6,8,1,2],[4,9]然后每个“市”任命“市长”,把右端点升为市长,管上本市村长的数字和: [ 1 , 8 , 3 , 11 ] , [ 5 , 13 , 3 , 18 ] , [ 6 , 8 , 1 , 10 ] , [ 4 , 9 ] [1,8,3,\textbf{11}],[5,13,3,\textbf{18}],[6,8,1,\textbf{10}],[4,9] [1,8,3,11],[5,13,3,18],[6,8,1,10],[4,9]注意:这时新上任的市长不需要“下基层”,只需要管上本区间的村长就行,原因是每个村长已经保存了本村的数字和了。也就是说,右端点只需要加上每个区间的第二个数就行了。接着继续合并为“省”: [ 1 , 8 , 3 , 11 , 5 , 13 , 3 , 18 ] , [ 6 , 8 , 1 , 10 ] , [ 4 , 9 ] [1,8,3,11,5,13,3,18],[6,8,1,10],[4,9] [1,8,3,11,5,13,3,18],[6,8,1,10],[4,9]选右端点为省长,管上本省的市长的数字和: [ 1 , 8 , 3 , 11 , 5 , 13 , 3 , 29 ] , [ 6 , 8 , 1 , 10 ] , [ 4 , 9 ] [1,8,3,11,5,13,3,\textbf{29}],[6,8,1,10],[4,9] [1,8,3,11,5,13,3,29],[6,8,1,10],[4,9]建树结束。最后得到的树状数组是: [ 1 , 8 , 3 , 11 , 5 , 13 , 3 , 29 , 6 , 8 , 1 , 10 , 4 , 9 ] [1,8,3,11,5,13,3,29,6,8,1,10,4,9] [1,8,3,11,5,13,3,29,6,8,1,10,4,9]之所以叫“树状数组”,是因为我们可以把“任命长官”的过程看成,把本区间的“低级官员”作为儿子节点接在新长官下面。整个区间就像一个树形结构一样,结合上面的图理解起来会更加深刻。

接下来我们就可以看出为什么这个结构查询前缀和会很快了。比如,我们想查 1 ∼ 7 1\sim7 17的前缀和,对应的原数组是 [ 1 , 7 , 3 , 0 , 5 , 8 , 3 ] [1,7,3,0,5,8,3] [1,7,3,0,5,8,3]。从建树过程,我们可以将其分成 [ 1 , 7 , 3 , 0 ] , [ 5 , 8 ] , [ 3 ] [1,7,3,0],[5,8],[3] [1,7,3,0],[5,8],[3]对应的树状数组是 [ 1 , 8 , 3 , 11 ] , [ 5 , 13 ] , [ 3 ] [1,8,3,\textbf{11}],[5,\textbf{13}],[\textbf{3}] [1,8,3,11],[5,13],[3]其中第一个区间的和,由"本市市长”得到,也就是 11 11 11,然后第二个区间的和,由“本村村长”得到,也就是 13 13 13,最后一个区间的和,就是它自己,也就是 3 3 3。接着将其求和,就得到了整个区间的和 27 27 27。结合上图中的树来看的话,其实就是对所要求的前缀区间右端点在树中所在“层”的数字向左求和:图中处于第 7 7 7位的 3 3 3,左边同层的数字是 13 13 13 11 11 11,求和就是 27 27 27

接下来的问题是,对于某个位置 x x x,怎么才能知道它的同层左边的数字位置呢?这就要用到 l o w b i t lowbit lowbit这个函数了,定义如下: l o w b i t ( x ) = x & ( − x ) lowbit(x)=x\&(-x) lowbit(x)=x&(x) x x x左边的同层节点的位置是 x − l o w b i t ( x ) x-lowbit(x) xlowbit(x) l o w b i t lowbit lowbit这个函数求的是 x x x二进制表示最低位的 1 1 1代表的数字。比如 l o w b i t ( 6 ) = l o w b i t ( ( 110 ) 2 ) = ( 10 ) 2 = 2 lowbit(6)=lowbit((110)_2)=(10)_2=2 lowbit(6)=lowbit((110)2)=(10)2=2。详细解释可以看这一篇https://blog.csdn.net/qq_46105170/article/details/103859795。关于如何理解为什么用 x − l o w b i t ( x ) x-lowbit(x) xlowbit(x)就能求出 x x x同层左节点,我们可以看上面的例子,还是查询 1 ∼ 7 1\sim7 17的前缀和,对应的原数组是 [ [ 1 , 7 , 3 , 0 ] , [ 5 , 8 ] , [ 3 ] ] [[1,7,3,0],[5,8],[3]] [[1,7,3,0],[5,8],[3]],将下标用二进制表示: [ [ 1 , 10 , 11 , 100 ] , [ 101 , 110 ] , [ 111 ] ] [[1,10,11,100],[101,110],[111]] [[1,10,11,100],[101,110],[111]]我们可以看到, 111 − l o w b i t ( 111 ) = 111 − 1 = 110 111-lowbit(111)=111-1=110 111lowbit(111)=1111=110,正好就是紧邻左边区间的右端点,继续对 110 110 110操作, 110 − l o w b i t ( 110 ) = 110 − 10 = 100 110-lowbit(110)=110-10=100 110lowbit(110)=11010=100,正好是再紧邻左边区间的右端点。深层次的原因是,区间是按照 2 2 2的幂次的长度分拆的,所以对于任意的下标位置 x x x,将 x x x二进制展开: x = a k 2 k + a k − 1 2 k − 1 + . . . + a 1 2 + a 0 = ( a k a k − 1 . . . a 1 a 0 ) 2 x=a_k2^k+a_{k-1}2^{k-1}+...+a_12+a_0=(a_ka_{k-1}...a_1a_0)_2 x=ak2k+ak12k1+...+a12+a0=(akak1...a1a0)2最后合并出来的区间其实是按照 x x x二进制的非零位划分的。比如说,如果 x = ( 10111 ) 2 x=(10111)_2 x=(10111)2,那分拆结果就是 [ 1 , 10000 ] , [ 10001 , 10100 ] , [ 10101 , 10110 ] , [ 10111 ] [1,10000],[10001,10100],[10101,10110],[10111] [1,10000],[10001,10100],[10101,10110],[10111]这其实就是一开始建树的时候,区间不停作合并,直到合并不下去的时候,所得到的结果。查询前缀和的步骤,也就是对树状数组先加 x x x处的数值,再加 x 1 = x − l o w b i t ( x ) x_1=x-lowbit(x) x1=xlowbit(x)处的数值,再加上 x 2 = x 1 − l o w b i t ( x 1 ) x_2=x_1-lowbit(x_1) x2=x1lowbit(x1)处的数值,如此一直累加,直到算出 x i − l o w b i t ( x i ) = 0 x_i-lowbit(x_i)=0 xilowbit(xi)=0

更新的过程正好和查询相反。查询是向左累加,而更新是向右更新,可以形象的比喻为不停“向上级汇报”。比如我们考虑之前例子的前几项: [ 1 , 7 , 3 , 0 , 5 , 8 , 3 , 2 ] [1,7,3,0,5,8,3,2] [1,7,3,0,5,8,3,2],假设我们要将 5 5 5加上 2 2 2,原数组变为 [ 1 , 7 , 3 , 0 , 7 , 8 , 3 , 2 ] [1,7,3,0,7,8,3,2] [1,7,3,0,7,8,3,2]。先看对应的树状数组:
[ 1 , 8 , 3 , 11 ] , [ [ 5 , 13 ] , 3 , 18 ] [1,8,3,11],[[5,\textbf{13}],3,\textbf{18}] [1,8,3,11],[[5,13],3,18]先将自己加上 2 2 2 [ 1 , 8 , 3 , 11 ] , [ [ 7 , 13 ] , 3 , 18 ] [1,8,3,11],[[7,\textbf{13}],3,\textbf{18}] [1,8,3,11],[[7,13],3,18]然后要将变化向上级汇报,也就是将“本村”的“村长”也加上 2 2 2 [ 1 , 8 , 3 , 11 ] , [ [ 7 , 15 ] , 3 , 18 ] [1,8,3,11],[[7,\textbf{15}],3,\textbf{18}] [1,8,3,11],[[7,15],3,18]接着继续向上汇报,将“本市”的“市长”也加上 2 2 2 [ 1 , 8 , 3 , 11 ] , [ [ 7 , 15 ] , 3 , 20 ] [1,8,3,11],[[7,\textbf{15}],3,\textbf{20}] [1,8,3,11],[[7,15],3,20]这样做的原因是很显然的:为了保持查询前缀和的时候仍然有效,我们只需要更新建树时“管辖”它的“长官”们,也就是合并区间时的每个右端点,或者在图中,就是沿着它的父亲节点向上爬。对于某个位置 x x x,它的父亲节点数字的位置也需要借助 l o w b i t lowbit lowbit函数: x + l o w b i t ( x ) x+lowbit(x) x+lowbit(x)。原因和之前的解释是类似的,这里就省略了。

具体代码如下:

#include <iostream>
#include <cstring>
using namespace std;

const int N = 5e5 + 10;
int n, m;
int tr[N];

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

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

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

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) {
        int x;
        scanf("%d", &x);
        add(i, x);
    }

    while (m--) {
        int type, x, y;
        scanf("%d%d%d", &type, &x, &y);
        if (type == 1) {
            add(x, y);
        } else {
            printf("%d\n", sum(y) - sum(x - 1));
        }
    }

    return 0;
}

查询和更新的复杂度都是 O ( log ⁡ n ) O(\log n) O(logn),理由很显然,因为做加减lowbit的操作最多只能做 log ⁡ n \log n logn次。至于建树,相当于对初始每位全为 0 0 0的Fenwick Tree做 n n n次update,所以是 O ( n log ⁡ n ) O(n\log n) O(nlogn)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值