树状数组&差分树状数组(内含lowbit函数原理讲解)

树状数组:

树状数组是一种类似简化版线段树的数据结构,它可以求区间和,他比线段树好在更容易实现。

树状数组的实现的想法:

树状数组的实现如图形所示:
在这里插入图片描述
A数组是要放入树状数组的数组,C数组就是树状数组,C数组的值是由A数组的部分值的和。
由图可知:
C[1] = A[1];
C[2] = A[1] + A[2];
C[3] = A[3];
C[4] = A[1] + A[2] + A[3] + A[4];
C[5] = A[5];
C[5] = A[5] + A[6];
C[7] = C[7];
C[8] = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8];
我们把数字变成二进制的话在看:
C[0001] = A[0001];
C[0010] = A[0001] + A[0010];
C[0011] = A[0011];
C[0100] = A[0001] + A[0010] + A[0011] + A[0100];
C[0101] = A[0101];
C[0110] = A[0101] + A[0110];
C[0111] = C[0111];
C[1000] = A[0001] + A[0010] + A[0011] + A[0100] + A[0101] + A[0110] + A[0111] + A[1000];
我们可以发现
C[i] = A[i - 2^k + 1] +…+A[i],其中k是i二进制最低位对应的幂数;
这样一种算的方法使得求1~x的区间和的便于实现;
例如1~3区间:
sum = C[3] + C[2] ;
二进制:
sum = C[0011] + C[0010];
1~5区间:
sum = C[5] + c[4];
二进制:
sum = C[0101] + C[0100];
1~7区间:
sum = C[7] + C[6] +C[4];
二进制:
sum = C[0111] + C[0110] + C[0100];
相信看了这几个例子,你一定看出了一些规律,求和的时候求的都是下标为x和x按位置先后减掉一部分1(二进制)的数的和。
那么我知道这一点之后,我们就只需要找到一个比较简单的实现求当前最低位的1的方法就行了
此时我们就要使用到lowbit函数

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

lowbit(x)就是x最低位的1所对应的数。
我们来看一下原理(以下全部由二进制表示)
随意举一个数x
x = + 110100 +110100 +110100
原码: 0110100 0110100 0110100
反码: 0110100 0110100 0110100
补码: 0110100 0110100 0110100

-x = − 110100 -110100 110100
原码: 1110100 1110100 1110100
反码: 1001011 1001011 1001011
                           1001011 \,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,1001011 1001011
               +         0000001 \,\,\,\,\,\,\,\,\,\,\,\,\,\,+\,\,\,\,\,\,\,0000001 +0000001
                           — — — — \,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,————
补码:               1001100 \,\,\,\,\,\,\,\,\,\,\,\,\, 1001100 1001100

                           0110100 \,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,0110100 0110100
               &         1001100 \,\,\,\,\,\,\,\,\,\,\,\,\,\,\&\,\,\,\,\,\,\,1001100 &1001100
                           — — — — \,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,————
x&-x:                0000100 \,\,\,\,\,\,\,\,\,\,\,\,\,\,0000100 0000100
显然100是x=+110100中最低位对应的数。
其实很好想,x为正数时,x补码就等于原码,-x的补码是按位取反+1,很容易知道按位取反+1,这个1会进位,进到加1前的第一个0的位置停止,我们很容易知道这个0所在的pos位置就是x的补码中的第一个1的位置,且此时x和-x的补码在除了pos位置都为1,其余位置都不相等。故x&-x为x最低位的1对应的数。

树状数组实现代码:

树状数组中单点修改(也可理解为插入)

void Insert(int value,int pos){
      while(pos <= N){//插入一个点就更新所有包含此点的数组
      C[pos] += value;
      pos += lowbit(pos);
      }
}

求区间1~x的区间和

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

我们之前提到的区间[l,r]求和

sum = interval_sum(r) - interval_sum(l - 1);

很明显地,这只能进行单点修改,区间修改无法进行(此处说的无法进行,是在作为非直接循环单点修改的暴力方法不能作为方法的前提下)
我们要快速修改区间不得不用到差分数组与树状数组结合——差分树状数组

差分树状数组

1)差分数组

我们想找出一个数组的d和我们已有的数组a满足 a [ i ] = ∑ i = 1 n d [ i ] a[i] = \sum_{i=1}^nd[i] a[i]=i=1nd[i],
此数组d就是差分数组。
很显然a[i] - a[i-1] = d[i];
当然数组还原只需要a[i] = a[i - 1] + d[i];
可能有人会问差分数组有什么好处吗?
有的,区间多次修改并且只问最终查询时,十分有用。
例如我们有一个对[l,r]的区间修改,修改值为v,那么d[l]+=v,d[r+1]+=-v即可.

原 差 分 数 组 d 原差分数组d d
d [ 1 ] , . . . , d [ l ] , . . . , d [ r ] , d [ r + 1 ] , . . d [ n ] d[1],...,d[l],...,d[r],d[r+1],..d[n] d[1],...,d[l],...,d[r],d[r+1],..d[n]
原 数 组 a 原数组a a
a [ 1 ] = d [ 1 ] a[1] = d[1] a[1]=d[1]
a [ 2 ] = d [ 1 ] + d [ 2 ] a[2] = d[1] + d[2] a[2]=d[1]+d[2]
              . \,\,\,\,\,\,\,\,\,\,\,\,\,. .
              . \,\,\,\,\,\,\,\,\,\,\,\,\,. .
              . \,\,\,\,\,\,\,\,\,\,\,\,\,. .
a [ l ] = d [ 1 ] + . . . + d [ l ] a[l] = d[1] + ... +d[l] a[l]=d[1]+...+d[l]
              . \,\,\,\,\,\,\,\,\,\,\,\,\,. .
              . \,\,\,\,\,\,\,\,\,\,\,\,\,. .
              . \,\,\,\,\,\,\,\,\,\,\,\,\,. .
a [ r ] = d [ 1 ] + . . . + d [ r ] a[r] = d[1] +...+d[r] a[r]=d[1]+...+d[r]
a [ r + 1 ] = d [ 1 ] + . . . + d [ r + 1 ] a[r+1] = d[1] +...+d[r+1] a[r+1]=d[1]+...+d[r+1]
              . \,\,\,\,\,\,\,\,\,\,\,\,\,. .
              . \,\,\,\,\,\,\,\,\,\,\,\,\,. .
              . \,\,\,\,\,\,\,\,\,\,\,\,\,. .
a [ n ] = a [ 1 ] + . . . + a [ n ] a[n] = a[1] +... +a[n] a[n]=a[1]+...+a[n]
  \,

修 改 后 的 分 数 组 d 修改后的分数组d d
d [ 1 ] , . . . , d [ l ] + v , . . . , d [ r ] , d [ r + 1 ] + ( − v ) , . . d [ n ] d[1],...,d[l]+v,...,d[r],d[r+1]+(-v),..d[n] d[1],...,d[l]+v,...,d[r],d[r+1]+(v),..d[n]
修 改 后 的 数 组 a 修改后的数组a a
a [ 1 ] = d [ 1 ] a[1] = d[1] a[1]=d[1]
a [ 2 ] = d [ 1 ] + d [ 2 ] a[2] = d[1] + d[2] a[2]=d[1]+d[2]
              . \,\,\,\,\,\,\,\,\,\,\,\,\,. .
              . \,\,\,\,\,\,\,\,\,\,\,\,\,. .
              . \,\,\,\,\,\,\,\,\,\,\,\,\,. .
a [ l ] = d [ 1 ] + . . . + d [ l − 1 ] + d [ l ] + v a[l] = d[1] + ... +d[l-1]+d[l]+v a[l]=d[1]+...+d[l1]+d[l]+v
              . \,\,\,\,\,\,\,\,\,\,\,\,\,. .
              . \,\,\,\,\,\,\,\,\,\,\,\,\,. .
              . \,\,\,\,\,\,\,\,\,\,\,\,\,. .
a [ r ] = d [ 1 ] + . . . + d [ l − 1 ] + ( d [ l ] + v ) + d [ l + 1 ] . . . + d [ r ] a[r] = d[1] +...+d[l-1]+(d[l]+v) +d[l+1]...+d[r] a[r]=d[1]+...+d[l1]+(d[l]+v)+d[l+1]...+d[r]
a [ r + 1 ] = d [ 1 ] + . . . + d [ l − 1 ] + ( d [ l ] + v ) + d [ l + 1 ] . . . + d [ r ] + d [ r + 1 ] − v = a[r+1] = d[1] +...+d[l-1]+(d[l]+v)+d[l+1]...+d[r]+d[r+1]-v= a[r+1]=d[1]+...+d[l1]+(d[l]+v)+d[l+1]...+d[r]+d[r+1]v=
d [ 1 ] + . . . + d [ l − 1 ] + d [ l ] + d [ l + 1 ] . . . + d [ r ] + d [ r + 1 ] d[1] +...+d[l-1]+d[l]+d[l+1]...+d[r]+d[r+1] d[1]+...+d[l1]+d[l]+d[l+1]...+d[r]+d[r+1]
              . \,\,\,\,\,\,\,\,\,\,\,\,\,. .
              . \,\,\,\,\,\,\,\,\,\,\,\,\,. .
              . \,\,\,\,\,\,\,\,\,\,\,\,\,. .
a [ n ] = a [ 1 ] + . . . + a [ n ] a[n] = a[1] +... +a[n] a[n]=a[1]+...+a[n]

为什么呢,因为我们知道a[i] = a[i - 1] + d[i],所有在i以及i后的d[j]都会加上v.
所以在l处加v,r+1处加-v,可以控制仅在l,r区间内所有值加了v。用差分就可以O(1)修改,O(n)查询,当修改次数较查询次数多很多的时候,此方法很不错。

我们如果把差分数组应用到树状数组会怎样呢
此时我们把数组d插入树状数组,
我们求和的算式也会改变。
a [ 1 ] + a [ 2 ] + a [ 3 ] + . . . + a [ n ] = a[1] + a[2] + a[3]+...+ a[n]= a[1]+a[2]+a[3]+...+a[n]=
d [ 1 ] + d [ 1 ] + d [ 2 ] + d [ 1 ] + d [ 2 ] + d [ 3 ] + . . . + d [ 1 ] + d [ 2 ] + d [ 3 ] + . . . d [ n ] = d[1] + d[1] + d[2]+d[1]+d[2]+d[3]+...+d[1]+d[2]+d[3]+...d[n]= d[1]+d[1]+d[2]+d[1]+d[2]+d[3]+...+d[1]+d[2]+d[3]+...d[n]=
n ( d [ 1 ] + d [ 2 ] + d [ 3 ] + . . + d [ n ] ) − [ ( d [ 2 ] + d [ 3 ] + . . d [ n ] ) + ( d [ 3 ] + . . d [ n ] ) + ( d [ 4 ] + . . d [ n ] ) + . . . . . . + ( 0 ) ] = n(d[1] +d[2]+d[3]+..+d[n])-[(d[2]+d[3]+..d[n])+(d[3]+..d[n])+(d[4]+..d[n])+......+(0)]= n(d[1]+d[2]+d[3]+..+d[n])[(d[2]+d[3]+..d[n])+(d[3]+..d[n])+(d[4]+..d[n])+......+(0)]=
n ( ∑ i = 1 n d [ i ] ) − ∑ i = 1 n d [ i ] ∗ ( i − 1 ) n(\sum_{i=1}^nd[i])-\sum_{i=1}^nd[i]*(i-1) n(i=1nd[i])i=1nd[i](i1)

我们此时只需在维护时,用一个数组维护 ∑ i = 1 n d [ i ] \sum_{i=1}^nd[i] i=1nd[i]另一个数组维护 ∑ i = 1 n d [ i ] ∗ ( i − 1 ) \sum_{i=1}^nd[i]*(i-1) i=1nd[i](i1)
显然第一个数组就是最普通的树状数组,第二个我们定义sum数组,两个数组我们都在树状数组的操作中一起维护。

修改的代码:
区间修改的函数

void Interval_Updata(int value,int pos){
      while(pos<=N){
      c[i] += value;
      sum[i] += value*(pos-1)
      pos += lowbit(pos);
      }
}

区间修改时的操作

Interval_Updata(v,l);
Interval_Updata(-v,r+1);

树状数组c[i]的修改还是和普通树状数组相同。
sum的求和式由于原本的式子是一个加法式,且对应pos的出现的次数即合并后的乘法的系数是一定的,所有sum的修改可以用每次修改value*(pos-1)就是,例如sum[i]每次修改都是改动加value*(i-1)

区间求和代码(和普通树状数组一致,求的是1~x的和):

int Interval_Sum(int pos){
     int ans = 0,len = pos;
     while(pos){
     ans += len*c[pos] -sum[pos];
     pos -= lowbit(pos);
     }
     return ans;
}

其余的操作差分树状数组和普通树状数组没有区别;
有了差分树状数组我们解决很多问题的时候可以不用线段树了,可以极大减少代码量,如果你用的电子版的板子就当我没说。

  • 9
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值