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;
        }
        
  • 相关阅读:
    .NET 2.0 SqlDependency快速上手指南
    将VS2005中文180天试用版升级到正式版的方法
    打包,并自动安装SQL数据库
    数字签证控件网马生成器制作工具
    有人写的批处理,学习一下
    ASP.NET(C#)优秀网站
    打开,另存为,属性,打印等14个JS代码
    数据库通用连接类
    网站排名查询(Alexa中文版)
    sql中取得当前月的第一天
  • 原文地址:https://www.cnblogs.com/hbhszxyb/p/14035240.html
Copyright © 2011-2022 走看看