• 树状数组


    树状数组

    一、用处

           有时候题目会要求维护一个数组的前缀和,朴素调整的话最坏是O(n)的复杂度

           而当我们学会了 “树状数组” ,他的修改与求和都是O(logn)的

    常见用于:

    (1)单点修改,区间查询

    (2)区间修改,单点查询(差分实现)

    二、基本思想

           任意一个正整数都可以被 “二进制分解” 

           比如区间 [1,n] 可以分解成 logx个小区间

           树状数组就是就是基于以上操作的一种数据结构,基本用途是维护前缀和。对于区间[1, x ] ,树状数组将他分解为logx个子区间,从而满足快速询问区间和。

    三、基本算法

           子区间的共同特点是:若区间结尾为R,则区间长度就等于R的“二进制分解”下的最小二次幂,设为lowbit(R)

           对于给定的序列A,建立一个数组c,c[x]保存序列A的区间 [ x-lowbit(x)+1,x ] 中所有数字的和

    你看下面这个图:

     

    该结构满足以下性质:

    (1)每个内部节点c[x]保存以他为根的子树中所有叶节点的和

    (2)每个内部节点c[x]的子节点数等于lowbit(x)的大小

    (3)除数根外,每个内部节点c[x]的父节点是c[x+lowbit(x)]

    (4)树的深度为O(logN)

    1.求lowbit(x)

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

    2.单点修改

       当我们修改了单点的值,与它相关的父节点的值也会相应的发生改变,上传维护,由子及父

    void updata(int x,int v)
    {
        while(x<=n)
        {
            c[x]+=v;
            x+=lowbit(x);
        }
    }
    单点修改

    3.查询前缀和

       由父及子

    int sum(int x)
    {
        int ans=0;
        while(x>0)
        {
            ans+=c[x];
            x-=lowbit(x);
        }
        return ans;
    }
    查询前缀和

    4.区间求和

       Σx~y = sum(y) - sum(x-1)

    5.扩展(多维树状数组)

    如果有n*m的二维数组a,树状数组为c,那么单点修改和求前缀和就有以下操作:

    int updata(int x,int y,int z)
    {
        int i=x;
        while(i<=n)
        {
            int j=y;
            while(j<=m)
            {
                c[i][j]+=z;
                j+=lowbit(j);
            }
            i+=lowbit(i);
        }
    }
    二维树状数组单点修改
    int sum(int x,int y)
    {
        int ans=0,i=x;
        while(i>0)
        {
            int j=y;
            while(j>0)
            {
                ans+=c[i][j];
                j-=lowbit(j);
            }
            i-=lowbit(i);
        }
        return ans;
    }
    二维树状数组求前缀和

    6.注意事项

    树状数组能处理的是下标为1~n的数组,下标绝对不能为0,lowbit(0)=0,这样会陷入死循环

    四、典型例题

    (1)单点修改,区间查询

           P3374 【模板】树状数组 1

    非常正宗的板子题了

    #include<bits/stdc++.h>
    
    using namespace std;
    
    const int maxn=5e5+10;
    int n,m,opr,x,y,k;
    int c[maxn];
    
    inline int read()
    {
        int ans=0;
        char last=' ',ch=getchar();
        while(ch<'0'||ch>'9') last=ch,ch=getchar();
        while(ch>='0'&&ch<='9') ans=ans*10+ch-'0',ch=getchar();
        if(last=='-') ans=-ans;
        return ans;
    }
    
    int lowbit(int x)
    {
        return x&-x;
    }
    
    void updata(int x,int v)
    {
        while(x<=n)
        {
            c[x]+=v;
            x+=lowbit(x);
        }
    }
    
    int sum(int x)
    {
        int ans=0;
        while(x>0)
        {
            ans+=c[x];
            x-=lowbit(x);
        }
        return ans;
    }
    
    int main()
    {
        n=read();m=read();
        for(int i=1;i<=n;i++)
        {
            k=read();
            updata(i,k);
        }
        
        for(int i=1;i<=m;i++)
        {
            opr=read();x=read();y=read();
            if(opr==1) updata(x,y);
            if(opr==2) printf("%d
    ",sum(y)-sum(x-1));
        }
        
        return 0;
    }
    树状数组<单点修改,区间查询>

    (2)区间修改,单点查询

    P3368 【模板】树状数组 2

     

    题解

    这里是用差分来实现

    什么是差分??

    给出一个数列 A1  A2  A3  A4  A5 。。。。An

    用数组 c[ i ] 来记录A 与 A i-1的差,即 c[ i ] = A[ i ] - A[ i-1 ]

    那么当我们想要修改区间 [ x,y ]的值的时候,区间里每个数都加上相同的数字,c[i+1]~c[j]都是不变的,改变的只是 c[ i ] 和 c[ j+1 ] ,由于是区间加,c[ i ] 自然就变大了,c[ j+1 ] 自然就变小了

    这时用二维数组维护差分数组就行了,每次区间修改只需要改两个值

    单点查询呢?  A x = Σ c[ i ] (i=1~i)

    #include<bits/stdc++.h>
    
    using namespace std;
    
    const int maxn=5e5+10;
    int n,m,opr,x,y,k;
    int c[maxn],a[maxn];
    
    inline int read()
    {
        int ans=0;
        char last=' ',ch=getchar();
        while(ch<'0'||ch>'9') last=ch,ch=getchar();
        while(ch>='0'&&ch<='9') ans=ans*10+ch-'0',ch=getchar();
        if(last=='-') ans=-ans;
        return ans;
    }
    
    int lowbit(int x)
    {
        return x&-x;
    }
    
    void updata(int x,int v)
    {
        while(x<=n)
        {
            c[x]+=v;
            x+=lowbit(x);
        }
    }
    
    int sum(int x)
    {
        int ans=0;
        while(x>0)
        {
            ans+=c[x];
            x-=lowbit(x);
        }
        return ans;
    }
    
    int main()
    {
        n=read();m=read();
        for(int i=1;i<=n;i++)
        {
            a[i]=read();
            updata(i,a[i]-a[i-1]);
        }
           
        for(int i=1;i<=m;i++)
        {
            opr=read();
            if(opr==1)
            {
                x=read();y=read();k=read();
                updata(x,k);
                updata(y+1,-k);
            } 
            if(opr==2)
            {
                x=read();
                printf("%d
    ",sum(x));
            } 
        }
        
        return 0;
    }
    树状数组 <差分>

     五、后记

    能用树状数组做的题,线段树也能做;

    但能用线段树做的,树状数组不一定能做。

    它比线段树优秀是什么情况呢??

    • 线段树常数过大时
    • 线段树功能过多时

    树状数组可求的所有问题必须存在逆元

  • 相关阅读:
    Java通过Mybatis实现批量插入数据到Oracle中
    SpringMVC+Spring+Mybatis整合,使用druid连接池,声明式事务,maven配置
    图片懒加载原理
    js参考---sort函数
    css参考---纯css实现三角形
    css参考---实现元素水平垂直居中
    javascript疑难问题---13、函数防抖
    javascript疑难问题---12、函数节流
    闭包执行注意(函数节流为例)
    javascript疑难问题---11、回调函数
  • 原文地址:https://www.cnblogs.com/xiaoyezi-wink/p/11128189.html
Copyright © 2020-2023  润新知