• 浅谈树状数组(解析+模板)


    也不知道是什么时候开始,对于曾经学过的算法都不太用了

    遇到区间修改,区间最值就知道用线段树,什么树状数组啊,st表啊都忘得差不多了

    最近几次模考被卡翻了,于是又想起这些老朋友

    来填个坑


    首先我们要明确一点,树状数组只能维护求和,不能维护区间最值

    树状数组利用了分治的思想,层数为logn,所以查询和修改都是logn,总复杂度为询问次数m乘logn

    也就是mlogn,最关键的是和线段树比起来,常数要小得多,跑的飞快

    而空间复杂度则是n的,只用开一维,还不用结构体

    但是树状数组的应用范围也相对较小

    通常分为两种

    (1)单点修改+取件查询

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

    具体为什么,我们一会儿会说到

    首先来张图片吧

    这是比较流行的一种图

    显而易见,树状数组是上面的C数组,而下面的A则是全数组,练出来的线代表每个节点的值是由那几个点组成的

    例如:C[4]=C[2]+C[3]+A[3]

    而我们如何找到组成当前节点的每一个点呢,或者说如何找到当前点的父亲呢

    这就引出了我们今天的重中之重

    lowbit函数

    来看一下这个函数长什么样子

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

    对的,只要压一下行就只有一行的小小的函数,就是整个树状数组的核心了

    虽然短,但是蕴含的内容却很不好理解,这个函数所求的是x化为二进制之后从末尾开始一共有几个零

    x加上这个数之后,就得到了他的父亲节点的下标

    减去这个数之后,就得到了上一个与x的子树不相交的根节点(因为建立是就是这样定义的)

    具体的原因与二进制中的补码有关,我们在这里就不详细说了,当个模板来背即可

    例如上图中,6+2=8    6-2=4

    而这两种不同的运算也就对应了树状数组中的两种操作

    操作一:单点修改

    首先我们可以知道当前要修改的点在原数组中的下标i,同时知道要加上(减去)的值v

    根据lowbit函数的定义我们可以知道,包含原数组中的值的节点的下标不可能小于原数组的下标

    同时改变某个点的值只会对其父亲有影响,所以理所应当的加上lowbit(i),直到根节点

    对于经过的每个节点,将权值加上v

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

    同样是压行之后只有一行

    操作二:区间查询

    和树状数组的含义有关,当前的树状数组中存的是类似于前缀和的东西

    所以我们很难得到一段区间的值,但是我们可以知道从1到x的值

    假设要查询的区间为[x,y],我们可以得到a[1]+a[2]+……+a[x-1],也可以同理得1到y

    做一下差会可以了

    具体的实现流程就是从当前点开始,不断减去lowit(i),知道节点1,将路径上的每一个点的值累加

    特别一题,树状数组中的下标不能为0,否则lowbit函数就会炸掉

    放一下操作代码(同样很简洁,这也是树状数组的优点之一)

    int solve(int i){
        int sum=0;
        for(;i>=1;i-=lowbit(i)) sum+=c[i];
        return sum;
    }

    以上就是树状数组的单点修改和区间查询

    来一道完整的题:树状数组模板1

    附上AC代码:

    #include<iostream>
    #include<cmath>
    #include<cstdio>
    #include<cstdlib>
    #include<cstring>
    #include<string>
    #include<algorithm>
    using namespace std;
    inline int min(int a,int b){return a<b?a:b;}
    inline int max(int a,int b){return a>b?a:b;}
    inline int rd(){
        int x=0,f=1;
        char ch=getchar();
        for(;!isdigit(ch);ch=getchar()) if(ch=='-') f=-1;
        for(;isdigit(ch);ch=getchar()) x=x*10+ch-'0';
        return x*f;
    }
    inline void write(int x){
         if(x<0) putchar('-'),x=-x;
         if(x>9) write(x/10);
         putchar(x%10+'0');
         return ;
    }
    int n,m;
    int c[500006];
    int lowbit(int x){return x&(-x);}
    void build(int i,int v){for(;i<=n;i+=lowbit(i)) c[i]+=v;}
    int solve(int i){
        int sum=0;
        for(;i>=1;i-=lowbit(i)) sum+=c[i];
        return sum;
    }
    int main(){
        n=rd(),m=rd();
        for(int i=1;i<=n;i++){
            int x=rd();
            build(i,x);
        }
        for(int i=1;i<=m;i++){
            int f=rd();
            int x=rd(),y=rd();
            if(f==1) build(x,y);
            else write(solve(y)-solve(x-1)),puts("");
        }
        return 0;
    }

    然后就是一个小小的修改了

    如何用树状数组来维护区间修改+单点查询

    大家可以先自己想一想(反正我当时是没有想出来的)

    不太会的同学不要担心

    因为这里的树状数组和我们刚才讲的不太一样

    哪里不一样呢,就是这里的C数组不是用来存和的

    而是被用来存一个叫做差分的东西

    什么是差分呢,就是对于一个数组

    我们不维护每个地方的值,而是维护一个前缀和

    使得从下标1加到下标x,就刚好可以得到原组的第x个元素

    虽然查询变慢了,但是区间修改只需要O(1)的时间

    为什么如此神奇呢,我们来举个例子

    现在我们需要将2到4的区间加上1

    我们就把差分数组下标为2的地方加1,下标为4+1的地方减1

    就变成了:

    计算前缀和,得到序列0 1 1 1 0 0 和原数组保持一致

    是不是很神奇呢

    而区间修改则是用树状数组来维护差分

    虽然把修改变成了logn

    但是相应的,单点查询也变成了logn

    看似血亏,实则血赚

    经过了上文的讲解,这里的具体操作我就不赘述了

    再来一道题:树状数组模板2

    附上AC代码:

    #include<iostream>
    #include<cmath>
    #include<cstdio>
    #include<cstdlib>
    #include<cstring>
    #include<string>
    #include<algorithm>
    using namespace std;
    inline int min(int a,int b){return a<b?a:b;}
    inline int max(int a,int b){return a>b?a:b;}
    inline int rd(){
        int x=0,f=1;
        char ch=getchar();
        for(;!isdigit(ch);ch=getchar()) if(ch=='-') f=-1;
        for(;isdigit(ch);ch=getchar()) x=x*10+ch-'0';
        return x*f;
    }
    inline void write(int x){
         if(x<0) putchar('-'),x=-x;
         if(x>9) write(x/10);
         putchar(x%10+'0');
         return ;
    }
    int n,m;
    int c[500006];
    int lowbit(int x){return x&(-x);}
    void build(int i,int v){for(;i<=n;i+=lowbit(i)) c[i]+=v;}
    int solve(int i){
        int sum=0;
        for(;i>=1;i-=lowbit(i)) sum+=c[i];
        return sum;
    }
    int main(){
        n=rd(),m=rd();
        int set=0;
        for(int i=1;i<=n;i++){
            int x=rd();
            build(i,x-set);
            set=x;
        }
        for(int i=1;i<=m;i++){
            int f=rd();
            if(f==1){
                int x=rd(),y=rd(),k=rd();
                build(x,k);build(y+1,-k);
            }
            else{
                int x=rd();
                write(solve(x)),puts("");
            }
        }
        return 0;
    }

    总而言之,树状数组还是很好的一种数据结构

    只要利用得当,每一种数据结构都能够焕发出耀眼的光芒,给代码带来无限生机

    蒟蒻总是更懂你✿✿ヽ(°▽°)ノ✿
  • 相关阅读:
    Apache ActiveMQ任意文件写入漏洞(CVE-2016-3088)复现
    Apache ActiveMQ序列化漏洞(CVE-2015-5254)复现
    Jenkins Java 反序列化远程执行代码漏洞(CVE-2017-1000353)
    jenkins未授权访问漏洞
    jenkins弱口令漏洞
    jboss 未授权访问漏洞复现
    jboss反序列化漏洞复现(CVE-2017-7504)
    C++中的强制类型转换
    new/delete与命名空间
    函数默认参数与占位参数
  • 原文地址:https://www.cnblogs.com/WWHHTT/p/9827053.html
Copyright © 2020-2023  润新知