zoukankan      html  css  js  c++  java
  • 【C++】浅析树状数组

    树状数组简介

    树状数组(Binary Indexed Tree(B.I.T), Fenwick Tree)是一个查询和修改复杂度都为(log n)的数据结构。主要用于查询任意两位之间的所有元素之和,但是每次只能修改一个元素的值;经过简单修改可以在(log n)的复杂度下进行范围修改,但是这时只能查询其中一个元素的值(如果加入多个辅助数组则可以实现区间修改与区间查询)。

    经过了如此一番看不懂的说明,或许你会直接绝望掉,But这东西贼重要,而且 这种东西竟然没有STL!!!气不气 QAQ

    基础概念

    假设数组(A_{1...n}),那么查询(sum_{j=1}^{i}A_j)的时间是(log)级别的,而且这是一个在线的数据结构,支持随时修改某个元素的值,复杂度也为(log)级别。

    来观察这个图:
    令这棵树的结点编号为(C_1,C_2,C_3......C_n)。令每个结点的值为这棵树的值的总和,那么容易发现:

    C1 = A1
    C2 = A1 + A2
    C3 = A3
    C4 = A1 + A2 + A3 + A4
    C5 = A5
    C6 = A5 + A6
    C7 = A7
    C8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8
    

    这里有一个有趣的性质:
    设节点编号为x,那么这个节点管辖的区间为(2^k)(其中k为x二进制末尾0的个数)个元素。

    这样说是不是就要明确一些了?

    根据这个性质,就有一个千古大罪人发明了树状数组

    代码实现

    大体结构

    struct B_I_T{
        int C[100001];
        int n;
        ......//函数
    };
    

    lowbit

    时间复杂度

    (O(1))

    实现

    还记得上面说的那一个性质吗?借助C++强大的位运算,我们可以在O(1)时间内求出(2^k)

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

    还有一种更简单也更常用的方式,是这样的

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

    struct B_I_T{
        int C[100001];
        int n;
        inline int lowbit(int x) { return x & -x; }
        ......//函数
    };
    

    就可以在结构体中加入这样一个函数了.
    不过,更常用的方法不是写在结构体里面,而是写在外面,否则不在结构体当中就只能用B_I_T::lowbit(x) 了,即

    inline int lowbit(int x) { return x & -x; }
    struct B_I_T{
        int C[100001];
        int n;
        ......//函数
    };
    

    其实还有一种实现方法,用宏定义:#define lowbit(x) (x & -x)

    lowbit的作用

    update
    时间复杂度

    (O(log n))

    意义

    (update(x,val)) => (A_x=A_x+val)

    实现
    void update(int x,int val) {
        for(int i=x;i<=n;i+=lowbit(x)) C[x]+=val;
    }
    

    这种使用for循环的做法,和下面使用while循环的原理是一样的.

    void update(int x,int val) {
        while(x<=n) {
            C[x]+=val;
            x+=lowbit(x);
        }
    }
    

    假设n=8,执行update(3,5),则有如下流程

    x=3 C[3]+=5 x=3+lowbit(3)=4
    x=4 C[4]+=5 x=4+lowbit(4)=8
    x=8 C[8]+=5 x=8+lowbit(8)=16
    x=16 退出

    对照上文的图片 我也不知道有多上文 我们可以知道,每一次C数组中执行加操作的下标,刚好都包括了(A_3)

    作用

    作用1——初始化

    inline void init() {
        scanf("%d",&n);
        for(reg int i=1;i<=n;i++) {
            scanf("%d",&a);
            update(i,a);
        }
    }
    

    作用2——改变单点的值 话说就是拿来干这件事的就不讲了

    query
    时间复杂度

    (O(log n))

    意义

    (query(x)) => (A_1+A_2+……+A_x)

    实现
    int query(int x) {
        int res=0;
        for(reg int i=x;i;i-=lowbit(x)) res+=C[i];
        return res;
    }
    

    这种使用for循环的做法,和下面使用while循环的原理是一样的.

    int query(int x) {
        int res=0;
        while(x) {
            res+=C[x];
            x-=lowbit(x);
        }
        return res;
    }
    

    假设n=8,执行query(7),则有如下流程

    res=0
    x=7 res=C[7]
    x=6 res=C[6]+C[7]
    x=4 res=C[4]+C[6]+C[7]
    x=0 退出

    仍然对照上(?)表,可以知道(C_4=A_1+A_2+A_3+A_4)(C_6=A_5+A_6)(C_7=A_7)。所以(C_4+C_6+C_7=sum^7_{i=1}A_i)

    作用

    作用1——求单点前缀和 不多赘述,直接(query(x))即可
    作用2——求区间和 (Sum(l,r)=query(r)-query(l-1)),即r的前缀和减去l-1的前缀和,即为l->r的区间和

    总结+代码

    将以上的所有总结在一起,可以有如下代码

    struct B_I_T{
        int n;
        int C[MAXN];
        inline int lowbit(int x) {
            return x & -x;
        }
        inline void update(int x,int val) {
            for(register int i=x;i<=n;i+=lowbit(i))
                C[i]+=val;
        }
        inline int query(int x) {
            int s=0;
            for(register int i=x;i;i-=lowbit(i))
                s+=C[i];
            return s;
        }
        inline void init() {
            for(register int i=1;i<=n;i++) {
                int a;
                scanf("%d",&a);
                update(i,a);
            }
        }
    };
    

    如此,我们就可以愉快地完成A+B problem了!

    逆序对

    话说这不一道归并的题吗?拿到B.I.T.这里来干哈的?
    先无脑打上mergesort的代码一份

    int n,a[500001];
    int s[500001];
    long long ans;
    void mrg(int left,int right,int mid)
    {
        int i=left,j=mid+1,k=left;
        while(i<=mid&&j<=right)
        {
            if(a[i]>a[j])
            {
                ans+=mid-i+1;
                s[k++]=a[j++];
            }
            else s[k++]=a[i++];
        }
        while(i<=mid)
            s[k++]=a[i++];
        while(j<=right)
            s[k++]=a[j++];
        for(int i=left;i<=right;i++)
            a[i]=s[i];
    }
    void mergesort(int left,int right)
    {
        if(left==right)return;
        int mid=(left+right)/2;
        mergesort(left,mid);
        mergesort(mid+1,right);
        mrg(left,right,mid);
    }
    

    离散化

    喂喂喂,这又是什么鬼?又关这道题什么事?不要急,先听我说。

    离散化是程序设计中一个常用的技巧,它可以有效的降低时间复杂度。
    有些数据本身很大,自身无法作为数组的下标保存对应的属性。如果这时只是需要这堆数据的相对属性,那么可以对其进行离散化处理。当数据只与它们之间的相对大小有关,而与具体是多少无关时,可以进行离散化。比如当你数据个数n很小,数据范围却很大时(超过1e9)就考虑离散化更小的值,能够实现更多的算法。

    Set an example:在这里插入图片描述

    方式

    离散化常见的两种方式: 1、数组离散化 2、用STL+二分离散化

    数组法
    for(register int i=1;i<=n;i++)
    {
        cin>>a[i].val;
        a[i].id = i;
    }
    sort(a+1,a+n+1);//定义结构体时按val从小到大重载
    for(int i=1;i<=n;i++)
        b[a[i].id]=i;//将a[i]数组映射成更小的值,b[i]就是a[i]对应的rank(顺序)值
    
    STL+二分
    //n原数组大小   num原数组中的元素    lsh离散化的数组    cnt离散化后的数组大小
    int lsh[MAXN],cnt,num[MAXN],n;
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&num[i]);
        lsh[i]=num[i];//复制一份原数组
    }
    sort(lsh+1,lsh+n+1);//排序,unique虽有排序功能,但交叉数据排序不支持,所以先排序防止交叉数据
    //cnt就是排序去重之后的长度
    cnt=unique(lsh+1,lsh+n+1)-lsh-1;//unique返回去重之后最后一位后一位地址-数组首地址-1
    for(int i=1;i<=n;i++)
        num[i]=lower_bound(lsh+1,lsh+cnt+1,num[i])-lsh;
    //lower_bound返回二分查找在去重排序数组中第一个等于或大于num[i]的值的地址-数组首地址,从而实现离散化
    

    实现

    逆序对实际上就是统计当前元素的前面有几个比它大的元素的个数,然后把所有元素比它大的元素总数垒加就是逆序对总数。


    代码

    int n;
    int C[500001];
    long long ans;
    int a[500001];
    int rnk[500001];
    inline int lowbit(int x) {
        return x & -x;
    }
    inline int query(int x) {
        int s=0;
        while(x>0) {
            s+=C[x];
            x-=lowbit(x);
        }
        return s;
    }
    inline void update(int x,int val) {
        while(x<=n) {
            C[x]+=val;
            x+=lowbit(x);
        }
    }
    int main() {
        for(reg int i=1;i<=n;i++) {
            a[i]=rnk[i]=read<int>();
        }
        sort(a+1,a+n+1);
        int cnt=unique(a+1,a+n+1)-a-1;
        for(reg int i=1;i<=n;i++) {
            rnk[i]=lower_bound(a+1,a+cnt+1,rnk[i])-a;
        }
        for(reg int i=1;i<=n;i++) {
            update(rnk[i],1);
            ans+=i-query(rnk[i]);
        }
        write(ans);
    }
    

    树状数组进阶

    差分

    如果你像我一样是个小白,那你也许看不懂这是什么东西。
    好吧,先上万能的百度百科

    差分,又名差分函数或差分运算,差分的结果反映了离散量之间的一种变化,是研究离散数学的一种工具,常用函数差近似导数。

    有没有仍然看不懂?
    那就直接来看一下差分的BIT吧

    区间修改+单点查询

    看起来有点正经的内容了

    主要思想

    令i~j的区间和为(a_i-a_{j-1}(i>j)),于是前缀和就为(a_i-a_0)。如果如此,那输入的时候就可以处理为(update(i,a_i-a_{i-1}))

    单点查询

    想一想,如果这样,(A_i=a_i-a_{i-1},A_1+A_2+...+A_i=a_i-a_0),就可以直接这样处理了。至于单点查询,可以很容易地想到,对于一个点i而言,因为a[0]=0,所以query(i)=a[i]

    区间修改

    直接上代码

    inline void update(int x,int delta) {
        ......//同上
    }
    inline void update(int l,int r,int delta) {
        update(l,delta); update(r+1,-delta);
    }
    

    想一想,如果(n=6)([2-4])这个区间加上4,那么如下

    id 1 2 3 4 5  6
    A  0 4 4 4 0  0
    C  0 4 0 0 -4 0
    

    震惊! 只有(C_2(+4))(C_5(-4))改变了!
    对于(C_3)的分析

    C[3]=A[3]-A[2]
    C'[3]=A'[3]-A'[2]=(A[3]+4)-(A[2]+4)=A[3]-A[2]=C[3]
    

    所以,按这样推来,只需要改动(C_l)(C_r)即可。

    区间修改+区间查询

    差分分析

    我们同样引入上面的差分C数组。
    (∵C_i=A_i-A_{i-1})
    (∴A_i=C_1+C_2+C_3+......+C_i)
    那么可以得到这一坨
    ( A_1+A_2+...+A_i)
    (=C_1+(C_1+C_2)+...+(C_1+C_2+...+C_i))
    (=i*C_1+(i-1)*C_2+...+C_i)
    (=i*(C_i+C_2+...+C_i)-1*C_2-...-(i-1)*C_i)
    所以,我们就可以再用一个毒瘤的差分数组(C1_i)来储存((i-1)*C_i)
    接上面的公式
    (=i*(C_1+C_2+C_3+......+C_i)-(C1_1+C1_2+C1_3+......+C1_i))

    代码

    typedef long long LL;
    #define lowbit(x) (x & -x)
    struct BIT{
        int n;
        LL C1[MAXN],C2[MAXN];
        inline BIT(){}
        inline void update(LL *C,int x,LL val) {
            while(x<=n) {
                C[x]+=val;
                x+=lowbit(x);
            }
        }
        inline void update(int l,int r,LL val) {
            update(C1,l,val); update(C1,r+1,-val);
            update(C2,l,val*(l-1)); update(C2,r+1,-val*r);
        }
        inline LL query(LL *C,int x) {
            LL s=0;
            while(x) {
                s+=C[x];
                x-=lowbit(x);
            }
            return s;
        }
        inline LL query(int l,int r) {
            return (l-1)*query(C1,l-1)-query(C2,l-1)-(r*query(C1,r)-query(C2,r));
        }
        inline void init(int N=0) {
            if(N) n=N;
            else n=read<int>();
            LL bef=0;
            for(reg int i=1;i<=n;i++) {
                LL num=read<LL>();
                update(C1,i,num-bef);
                update(C2,i,(num-bef)*(i-1));
                bef=num;
            }
        }
    };
    

    2D树状数组

    怎么会有这么毒瘤的题……

    int n,m;
    struct BIT2D{
        int C[1001][1001];
        ......//函数
    };
    

    query

    分析——放弃
    代码——

    inline int query(int x,int y) {
        int res=0;
        for(register int i=x;i;i-=lowbit(i)) {
            for(register int j=y;j;j-=lowbit(j)) {
                res+=C[i][j];
            }
        }
        return res;
    }
    

    update

    分析——同上
    代码——

    inline void update(int x,int y,int delta) {
        for(register int i=x;i<=n;i+=lowbit(i)) {
            for(register int j=y;j<=n;j+=lowbit(j)) {
                C[i][j]+=delta;
            }
        }
    }
    

    区间修改+单点查询

    一句话总结:(C_{i,j}=A_{i,j}-A_{i-1,j}-A_{i,j-1}+A_{i-1,j-1})

    区间修改+区间查询

    同样是一句话:维护(C_{i,j})(i*C_{i,j})(j*C_{i,j})(i*j*C_{i,j})

    时间复杂度

    每一次操作为(O(log^2 n))

    当然了,更高维的树状数组也可以此类推,每一次操作的时间复杂度为(O(log^k n))(k是树状数组的维度)

    罗列例题

    一维

    多维

    完结撒花!!!!!!!!!

  • 相关阅读:
    vscode 多文件编译
    Spring
    tomcat server.xml详细解析
    XML解析——Java中XML的四种解析方式
    MyBatis-config配置信息
    java学习笔记--JDBC实例
    50道经典的JAVA编程题(目录)
    Java8 函数式编程详解
    递归,--遍历多维数组
    eslint关闭配置--vue-webpack
  • 原文地址:https://www.cnblogs.com/PI-UKE/p/13566872.html
Copyright © 2011-2022 走看看