树状数组及其应用

138 阅读4分钟

说起树状数组,那么首先要掌握差分和前缀和这两个概念(大部分题就相关)

前言

差分和前缀和的概念是相反的,原数组是差分数组的前缀和

也就是原数组a[],差分数组d[],满足d[i]=a[i]-a[i-1];

a[i]=d[1]+....+d[i]

前缀和相反

树状数组

树状数组我感觉就是对差分数组的一种二进制压缩,原本来说差分数组查询原数组的时间复杂度为n,经过差分数组的压缩变为log2n

他是利用二叉树的样式来存储

切入点:lowbit函数

由于电脑一种叫做补码的操作(由于电脑是二进制,它们存的相反数是它的取反+1),一个数与它的相反数做与操作时会返回二进制下最右边的1的位置。举个例子: 6&-6=2 将6变成二进制:110。其中最右侧的1加粗字体:110则返回的是二进制下10的值:2。得到代码:

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

利用这个性质建立树状数组:

为了简化区间修改的效率,我们需要建立这样的一个数组:数组中第k位的值为原数组中的一段区间和,这个区间的长度是lowbit(k),终点是k。 比如:输入一个数组,那么我们所建立的数组:

  • 第一位(1在二进制下=1 二进制下的1=1)的值为输入的数组的第一位往前的一位的和,也就是第一位。
  • 第二位(2在二进制下=10 二进制下的10=2)的值为输入的数组的第二位往前两位的和,第一位和第二位。
  • 第三位(3在二进制下=11 二进制下的1=1)的值为输入的数组的第三位往前一位的和,也就是第三位。
  • 第四位的值(4在二进制下=100 二进制下的100=4)的值为输入的数组的第四位往前四位的和,也就是第一位,第二位,第三位以及第四位。

我们就称这种数组为树状数组

static void update(int x,int val){ //x:修改的位置 val: 增加的值
    while (x<=N){
        tree[x]+=val;
        x+=lowbit(x);
    }
}

这样就很好传递了,有点动归的意思哈

相当于7二进制位111,利用lowbit查询到最后一个1的位置为1减去为110(6),重复得到100(4),重复得到000(舍去)这样sum[7]=tree[6]+tree[4]+tree[7],这样修改后的原数组查询就大大减少时间复杂度了

static int sum(int x){
int ans=0;
while (x!=0){
    ans+=tree[x];
    x-=lowbit(x);
}
return ans;
}

修改区间:

根据题意去设定tree存储的是前缀和还是差分

1.单点修改+区间和查询使用前缀和的思想

for (int i = 1; i <= n; i++){
    int  x=sc.nextInt();
    //初始化数据
    update(i,x);
}
for (int i = 1; i <= m; i++){
    int op=sc.nextInt();
    if(op==1){
        //修改第a,+k
        int a=sc.nextInt();
        int k=sc.nextInt();
        update(a,k);
    }
    if(op==2){
        //查询区间{a,b}
        int a=sc.nextInt();
        int b=sc.nextInt();
        System.out.println(sum(b)-sum(a-1));
    }

2.区间修改+单点查询使用差分的思想

 for (int i = 1; i <= n; i++){
            int x=sc.nextInt();
     //初始化数据
            update(i,x);
            update(i+1,-x);
        }
        for (int i = 1; i <= m; i++){
            int op=sc.nextInt();
            if(op==1){
            //修改[a,b]区间内+k
                int a=sc.nextInt();
                int b=sc.nextInt();
                int k=sc.nextInt();
                update(a,k);
                update(b+1,-k);
            }
            if(op==2){
                //查询
                int a=sc.nextInt();

                System.out.println((long) sum(a));
            }
        }

这两个的区别就在于初始化tree数组的方法

3.区间修改+区间查询

可以使用线段树的方式去解答

这里采用差分的思想去解答(因为差分可以对原数组进行修改查询)

但是区间和和差分相差两段累加和的结果.但是我们不难发现,区间和和差分数组还是有关系的

tree[1]+tree[2]+……tree[i]=a[i]

那么对于区间[l,r]

a[1]+a[2]+……+a[r-1]+a[r] //用上方公式推导得出

=tree[1]+(tree[1]+tree[2])+……+(tree[1]+……+tree[r]) //根据加法交换律与结合律:

=(tree[1](r))+(tree[2](r-1))+……(tree[r]*1) //那么:

=r*(tree[1]+tree[2]+……+tree[r])-(tree[1]*0+tree[2]1+……+tree[r](r-1))

这样我们一样可以通过sum来得到区间和

单独用tree1[i]=tree[i]*(i-1)存储,然后计算tree1的前缀和就行了

代码:

for (int i = 1; i <= n; i++) {
    long  x=sc.nextInt();
    update(i,x-old,tree1);
    update(i,(i-1)*(x-old),tree2);
    old=x;
}
while (m-->0){
    int op=sc.nextInt();
    if(op==1){
        long l=sc.nextInt();
        long r=sc.nextInt();
        long k=sc.nextInt();
        update(l,k,tree1);
        update(r+1,-k,tree1);
        update(l,(l-1)*k,tree2);
        update(r+1,-r*k,tree2);
    }else {
        long l=sc.nextInt();
        long  r=sc.nextInt();
        System.out.println(r*sum(r,tree1)-sum(r,tree2)-(l-1)*sum(l-1,tree1)+sum(l-1,tree2));
    }
}