天天看点

树状数组简介基本思想具体原理例题

  • 简介
  • 基本思想
  • 具体原理
    • 数据结构
    • lowbit函数的实现
    • 修改一个元素的值
    • 查询
  • 例题
    • 树状数组1
      • 题目描述
      • 输入输出格式
        • 输入格式:
        • 输出格式:
      • 标程
    • 树状数组2
      • 题目描述
      • 输入输出格式
        • 输入格式:
        • 输出格式:
      • 标程

简介

树状数组这个东西。。。有人说它像线段树。。。

其实感觉两者并没有什么直接联系。。。但是都是类似于树形操作的思想,所以经常把他俩放在一块说。。。

稍微讲讲我对这个算法的见解,大家看看对不对。。。

基本思想

基本思想是这样的:

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

比如下面这个图(图中的数字代表节点的下标):

树状数组简介基本思想具体原理例题

我们假设a[MAXN]数组用来存储初始数据,e[MAXN]代表了树状数组存储内容。

例如在上图的树状数组中,e[8]号记录了a[1]…a[8]的和,即e[4]+e[6]+e[7]+a[8]。绿色的线代表树的节点从哪些节点求和得到。

所以根据以上性质,树状数组实现的功能有:

  • 将一个数组转化成树状数组
  • 改变某一个点的值
  • 询问a[1]+a[2]+······+a[x]的值

具体原理

具体的就是二进制的原理,比较绕。。。先从数据结构开始讲。。。

数据结构

看图,这个图是上面那个图的所有树状数组节点编号变成二进制以后的样子:

树状数组简介基本思想具体原理例题

你有木有发现什么蹊跷之处?(并木有发现)

树状数组的节点深度其实就是它的节点编号的二进制形式中,从右往左数第一个1出现的位置。(这谁能发现啊喂!)

比如说:

6的二进制形式中(110),从右往左数第一个1出现的位置是2

7的二进制形式中(111),从右往左数第一个1出现的位置是1

8的二进制形式中(1000),从右往左数第一个1出现的位置是4

我们定义寻找这个“数字的二进制形式中,从右往左数第一个1出现的位置”(啊啊啊这个名字好长啊啊啊啊)的函数为 lowbit(x) l o w b i t ( x ) 函数。

即:

lowbit(6)=2 l o w b i t ( 6 ) = 2

lowbit(7)=1 l o w b i t ( 7 ) = 1

lowbit(8)=4 l o w b i t ( 8 ) = 4

然后你还会发现,一个节点并不一定是代表自己前面所有元素的和。只有满足 2n 2 n 这样的数才代表自己前面所有元素的和。

那么归纳一下,就能得出结论:

设节点的编号为 x x ,那么这个的值等于 a[x−2lowbit(x)−1+1]a[x−2lowbit(x)−1+1] 到 a[x] a [ x ] 的和。

比如说 e[6]=a[6−22−1+1]+a[6]=a[5]+a[6] e [ 6 ] = a [ 6 − 2 2 − 1 + 1 ] + a [ 6 ] = a [ 5 ] + a [ 6 ]

再比如说 e[8]=a[8−24−1+1]+a[8−24−1+2]+⋅⋅⋅+a[8]=e[4]+e[6]+e[7]+a[8] e [ 8 ] = a [ 8 − 2 4 − 1 + 1 ] + a [ 8 − 2 4 − 1 + 2 ] + · · · + a [ 8 ] = e [ 4 ] + e [ 6 ] + e [ 7 ] + a [ 8 ]

这就是树状数组比较神奇的地方之一。

lowbit函数的实现

这个地方用到的姿势是我难以去证明的。。。

但是我只能告诉你:

lowbit(x)=(−x) l o w b i t ( x ) = ( − x ) & x x

在这里要注意:

  • ‘&’符号是按位取“与”运算。比如:10111011& 0110=0010 0110 = 0010
    • 负数进行按位取“与”运算的时候,是对负数的“补码”进行的。
    • 什么是补码?以下选自百度百科

      计算机中的符号数有三种表示方法,即原码、反码和补码。三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位,三种表示方法各不相同。

      补码具有的性质:

      1、对一个整数的补码再求补码,等于该整数自身。

      2、补码的正零与负零表示方法相同。

      求给定数值的补码分以下两种情况:

      正数

      正整数的补码是其二进制表示,与原码相同 。

      【例】+9的补码是00001001。

      (备注:这个+9的补码是用8位2进制来表示的,补码表示方式很多,还有16位二进制补码表示形式,以及32位二进制补码表示形式,64位进制补码表示形式等。每一种补码表示形式都只能表示有限的数字。)

      负数

      求负整数的补码,将其对应正数二进制表示所有位取反(包括符号位,0变1,1变0)后加1。

      同一个数字在不同的补码表示形式中是不同的。比如-15的补码,在8位二进制中是11110001,然而在16位二进制补码表示中,就是1111111111110001。以下都使用8位2进制来表示。

      【例】求-5的补码。

      -5对应正数5(00000101)→所有位取反(11111010)→加1(11111011)

      所以-5的补码是11111011。

      然后嘛,补码,按位与运算。

      啊啊啊我也是真的不知道为什么会这么神奇啊。。。反正这样一计算就得到“一个数的二进制形式中,从右往左数第一个1出现的位置”了。。。

      比如:

      lowbit(6)=00000110 l o w b i t ( 6 ) = 00000110 & 11111010=00000010=2 11111010 = 00000010 = 2
      真是十分有趣。。。脑子是个好东西,真希望我有。。。

      修改一个元素的值

      因为树状数组的特殊性质,我们只需要修改所有包含这个元素的节点就行了。

      那怎么操作呢?

      假设本次对 e[x] e [ x ] 进行了操作,那么下一次就对 e[x+lowbit(x)] e [ x + l o w b i t ( x ) ] 进行操作就行了。

      至少看图是这样没错。论证。。。(此处省略 101010 10 10 10 字论证过程,其实我不会233333)

      好吧。。。没有论证。。。CPP代码上来!

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

      查询

      查询是为了得到 a[1]+a[2]+⋅⋅⋅⋅⋅⋅+a[x] a [ 1 ] + a [ 2 ] + · · · · · · + a [ x ] 的值。

      同样是看图。。。同样是因为一些非常特殊的性质。。。

      所以。。。讲一下怎么进行一波操作。。。我的能力无法证明它。。。

      对于目标 x x 。。。

      每一次操作:

      答案加上xx对应节点的值,然后将 x x 减去它的lowbitlowbit,继续进行这样的操作,直到 x x 小于等于00。
    不信。。。用图片验证一下吧。。。我也证明不了了。
    int query(int x)
    {
        int sum=;
        while(x>)
        {
            sum+=e[x];
            x-=lowbit(x);
        }
        return sum;
    }
               

    例题

    树状数组1

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

    题目描述

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

    1.将某一个数加上x

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

    输入输出格式

    输入格式:

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

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

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

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

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

    输出格式:

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

    标程

    贴上我的标程。。。

    其实也就是完整的树状数组。。。

    #define MAXN 500005
    class TA
    {
        private:
            int e[MAXN];
            int len;
            int lb(int k)
            {
                return k&(-k);
            }
        public:
            void add(int x,int v)
            {
                while(x<=len)
                {
                    e[x]+=v;
                    x+=lb(x);
                }
            }
            void init(int* getin,int _len)
            {
                len=_len;
                for(int i=;i<=len;i++)
                {
                    add(i,*(getin+i-));
                }
            }
            int query(int x)
            {
                int sum=;
                while(x>)
                {
                    sum+=e[x];
                    x-=lb(x);
                }
                return sum;
            }
    };
    TA ta;
    int a[MAXN],n,m;
    
    int main()
    {
        scanf("%d%d",&n,&m);
        for(int i=;i<=n;i++)
        {
            scanf("%d",&a[i]);
        }
        ta.init(a+,n);
        for(int i=;i<=m;i++)
        {
            int ope,a,b;
            scanf("%d%d%d",&ope,&a,&b);
            if(ope==)
            {
                ta.add(a,b);
            }
            else
            {
                cout<<ta.query(b)-ta.query(a-)<<endl;
            }
        }
        return ;
    }
               

    树状数组2

    洛谷-【模板】树状数组 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的结果。

    标程

    这个题不仅是树状数组,当然还使用了线段树的思想。

    修改的时候,只需要将组成这个区间的几个节点加这个数(类似于线段树中的懒操作)。

    查询的时候,依层找自己的上级,然后加上自己上级的值就行了。因为在这里,上级的值就相当于懒操作的值。

    #include<iostream>
    #include<cstdio>
    #include<algorithm>
    using namespace std;
    
    #define MAXN 500005
    
    int lb(int k)
    {
        return k&(-k);
    }//lowbit 
    
    int e[MAXN];
    int a[MAXN],n,m;
    
    void addto(int x,int v)
    {
        while(x>)
        {
            e[x]+=v;
            x-=lb(x);
        }
    }//实现1-x区间加v 
    
    int query(int x)
    {
        int ans=a[x];
        while(x<=n)
        {
            ans+=e[x];
            x+=lb(x);
        }
        return ans;
    } 
    
    int main()
    {
        scanf("%d%d",&n,&m);
        for(int i=;i<=n;i++) scanf("%d",&a[i]);
        for(int i=;i<=m;i++)
        {
            int operate;
            scanf("%d",&operate);
            if(operate==)
            {
                int a,b,v;
                scanf("%d%d%d",&a,&b,&v);
                addto(b,v);
                addto(a-,-v);
            }
            else
            {
                int x;
                scanf("%d",&x);
                cout<<query(x)<<endl;
            }
        }
        return ;
    }
               

继续阅读