树状数组

树状数组

线段树问题集合包含数树状数组,这意味着树状数组能解决的问题线段树往往也能解决。

image-20201013181245816

虽然用法上,线段树比树状数组丰富很多,但是树状数组也有它的优势:1. 代码短;2. 运行效率很高(相对于线段树,常数非常小,大部分情况下能差 10 倍左右,如果是简单问题的话,常数会差的稍微少一些。)

方法选择上,如果能用树状数组来做,就不要用线段树。

树状数组

树状数组的作用

可以动态、快速地(O(log n))求前缀和,我们一般情况下就是用树状数组来求前缀和的 ~

动态、快速地求前缀和:设原数组为 a,其对应的前缀和数组为 s,传统求前缀和的方法求出前缀和后不能改变原数组的某个元素 a[i],否则 s[i] 就不再是 a[1] ~ a[i] 的和。但是若使用树状数组来构造前缀和,则当 a[i] 更新时,其前缀和 s[i] 会自动地被更新,而且效率是 O(log n) 。

树状数组支持的两种操作效率都是 O(log n)

  1. 单点修改(修改原数组 a)
  2. 区间查询(动态查询)

利用上述操作再配合差分思想,可以推出其它操作:

  1. 区间修改
  2. 单点查询
  3. 区间查询

树状数组的原理

树状数组算法中,原数组下标一定要从 1 开始,这点与前缀和算法一样。

image-20201013183344845

**树状数组也是一个 一维数组,长度与原数组一样长 。**后续,我们只是在逻辑上画了很多层,但是物理结构依旧是一维的。

设树状数组为 c,则树状数组所有奇数序号都存储原数组 a 对应序列的值。image-20201013183745753

这一层被称为 第 0 层,即 2 0 2^{0} 20 层。image-20201013183907723

第 k 层的元素定义:树状数组 c 序号中凡是能被 2 k 2^{k} 2k整除,但是不能被 2 k + 1 2^{k+1} 2k+1整除的序号值(本质是看序号的二进制表示的数,末尾有几个 0,末尾有 k 个 0 就是 第 k 层),都归为第 k 层。

在第 k 层中,假设数组数组 c 的任意序号值为 c[i],则 c[i] 的值必定包含 a[i],但是其它部分的值必须根据 c[i] 所在的层数累加 c,累加 c 的元素个数 为 k。如下图:

image-20201013190113596

比如:

c[2] = a[2] + c[1]; // 第 1 层
c[4] = a[4] + c[3] + c[2]; // 第 2 层
c[6] = a[6] + c[5]; // 第 1 层
c[8] = a[8] + c[7] + c[6] + c[4]; // 第 3 层
c[10] = a[10] + c[9]; // 第 1 层
c[12] = a[12] + c[11] + c[10]; // 第 2 层
c[14] = a[14] + c[13]; // 第 1 层
c[16] = a[16] + c[15] + c[14] + c[12] + c[8]; // 第 4 层

根据这个定义,我们可以发现,数树状数组 c 中存的数,其实是一段数据的和。c[i] 表示的是原数组 a 中,区间 ( i − 2 k , i ] ( i-2^{k}, i ] (i2k,i] 所有元素的和。

根据上文,我们知道 k 表示 序列号的二进制表示右边共有 k 个 0,那么我们可以使用 lowbit运算 来计算 2 k 2^{k} 2k

lowbit(i) = i & -i; // 求得 2^k 的值,其中 k 表示 数字 i 的二进制表示末尾有 k 个 0
int lowbit(int i) {
    return i & -i;
}

因此,上述区间表示为 ( i − l o w b i t ( i ) , i ] ( i - lowbit(i), i ] (ilowbit(i),i]

根据上述得到的结果,我们再来看看树状数组的两个最基本的操作如何实现。

  1. 求 1 ~ i 的前缀和

    因为 c[i] 表示原数组 a 区间范围 ( i − l o w b i t ( i ) , i ] (i - lowbit(i), i] (ilowbit(i),i] 的和,所以需要使用递归函数,每次累加 c[xi],其中 xi-1 = xi - lowbit(xi)

    /* 求[1, x] 区间的和 */
    int sum(int x) {
    	int result = 0;
        for (int i = x; i > 0; i -= lowbit(i)) { // 对于二进制数字n,最多logn 个0,所以时间复杂度 O(log n)
            result += c[i];
        }
        return result;
    }
    
  2. 给某个位置加上一个数 v,并更新前缀和。

    我们发现,当更新一个位置 i 时,实际上影响的 c 元素是( 设 th = i, 则 th+1= th + lowbit(th) )所有合法范围的 thimage-20201013193013992

    void add(int x, int v) {
       a[x] += v; // 更新原数组
    	
       //* i 每次加lowbit(i)意味着末尾多了一个0,对于数字n,最多有log n 个0,
       //* 所以为 O(log n)
       for (int i = x; i <= n; i += lowbit(i)) 
           c[i] += v; // 更新树状数组
    }
    

初始化树状数组

void initializeC(int x) {
    int limit = x - lowbit(x);
    for (int i = x; i > limit; --i ) { // c[x] 表示 a 数组区间 (x-lowbit(x), x] 范围的所有元素和
        c[x] += y[i];
    }
}

或者直接使用 add 函数也行

void add(int x, int v) {
    a[x] += v;

    for (int i = x; i <= n; i += lowbit(i)) {
        c[i] += v;
    }
}

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

Acwing

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值