zoukankan      html  css  js  c++  java
  • 树状数组

    树状数组

    一、适用范围

    • 树状数组是一个查询和修改复杂度都为 (log(n)) 的数据结构,常常用于查询任意区间的所有元素之和。
    • 与前缀和的区别是支持动态修改, (log(n)) 的时间进行修改,(log(n)) 查询。
    • 支持如下操作:
      • 单点修改区间查询
      • 区间修改单点查询
      • 区间修改区间查询

    二、算法原理

    1. 树状数组较好的利用了二进制。它的每个节点的值代表的是自己前面一些连续元素。至于到底是前面哪些元素,这就由这个节点的下标决定。

    1. 设节点的编号为 (i) ,那么:

    [c[i]=sum_{j=i-lowbit(i)+1}^i a[j] ]

    1. 即可以推导出:

      C[1] = A[1]  # lowbit(1)个元素之和
      C[2] = C[1] + A[2] = A[1] + A[2]  # lowbit(2)个元素之和
      C[3] = A[3]  # lowbit(3)个元素之和
      C[4] = C[2] + C[3] +A[4] = A[1] + A[2] + A[3] + A[4] # lowbit(4)个元素之和
      C[5] = A[5]
      C[6] = C[5] + A[6] = A[5] + A[6]
      C[7] = A[7]
      C[8] = C[4] + C[6] + C[7] + A[8] = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8]
      
    2. 显然一个节点并不一定是代表自己前面所有元素的和。只有满足 (2^n) 这样的数才代表自己前面所有元素的和。

    3. 理解 (lowbit) 函数

      • 原码:如果机器字长为 (n),那么一个数的原码就是用一个 (n) 位的二进制数,其中最高位为符号位:正数为 (0),负数为 (1)。剩下的 (n-1) 位表示该数的绝对值。

      • 反码:知道了原码,那么你只需要具备区分 (0)(1) 的能力就可以轻松求出反码,为什么呢?因为反码就是在原码的基础上,符号位不变其他位按位取反(就是 (0)(1)(1)(0))就可以了。

      • 补码也非常的简单,就是在反码的基础上按照正常的加法运算加 (1) 。正数的补码就是其本身。负数的补码是在其原码的基础上符号位不变,其余各位取反,最后 (+1),即取反 (+1)

      • $lowbit(x)=x&-x $ :表示截取 (x) 二进制最右边的 (1) 所表示的值,可以写成函数或宏定义

      • 注意宏定义是括号,因为宏名只是起到一个替代作用,不加括号在运算时优先级会出问题

        //1. 宏定义,注意括号,不建议这样写,容易产生歧义
        #define lowbit(x) ((x) & -(x))
        //2. 函数写法,推荐写法:
        int lowbit(int x){return x & -x;}
        

    三、 树状数组的操作

    1. (update) 更新操作

      • 因为树状数组 (c[x]) 维护的是一个或若干个连续数之和,当我们修改了 (a[x]) 之后,(xsim n) 前缀和均发生了变化,所以除了(c[x]) 需要修改之外 (x) 的祖先节点也必须修改而 (x) 的父亲节点为 (x+lowbit(x)),我们叫向上更新。

      • 把序列中第 (i) 个数增加 (x)(sum[i]sim sum[n]) 均增加了 (x) ,所以我们只需把这个增量往上更新即可。如果,把 (a[i]) 修改成 (x),则我们向上更新 (a[i]) 的增量:(x-a[i])

        //1. a[id] 增加 x while写法
        void updata(int id,int x){
            while(id<=n){//向上更新,更新到n为止
                c[id]+=x;
                id+=lowbit(id);
            }
        }
        //2. a[id] 修改成 x  for写法
        void updata(int id,int x){//或者传递参数是x=x-a[id],此时跟第一种写法一样
            for(int i=id;i<=n;i+=lowbit(i))
                c[i]+=x-a[id];
        }
        
    2. (getsum) 查询操作

      • 因为树状数组维护的是一个能够动态修改的前缀和,所以可以在 (log(n)) 的效率下求出前 (n) 项和(sum[i])

      • 如果 (i=2^j (j=0,1,..n)), 此时最简单,显然有:(sum[i]=c[i]) ,如果 (i) 是其他的情况呢?

        • (sum[5]=c[5]+c[4] (4=5-lowbit(5)))
        • (sum[15]=c[15]+c[14]+c[12]+c[8] (14=15-lowbit(15),12=14-lowbit(14),...))
      • 显然,想要求出前 (i) 项前缀和 (sum[i]) ,只需沿着当前节点向下累加直到节点编号为 (2^j) 为止。我们叫向下求和。

        int getsum(int id){
            int tot=0;
            for(int i=id;i>0;i-=lowbit(i))
                tot+=c[i];
            return tot;
        }
        
  • 相关阅读:
    1062 Talent and Virtue (25 分)
    1083 List Grades (25 分)
    1149 Dangerous Goods Packaging (25 分)
    1121 Damn Single (25 分)
    1120 Friend Numbers (20 分)
    1084 Broken Keyboard (20 分)
    1092 To Buy or Not to Buy (20 分)
    数组与链表
    二叉树
    时间复杂度与空间复杂度
  • 原文地址:https://www.cnblogs.com/hbhszxyb/p/14035240.html
Copyright © 2011-2022 走看看