树状数组
树状数组,就是用数组来模拟树形结构。
可以方便的实现单点更新,单点查询,区间查询,区间更新
常数比线段树小很多,也比线段树好打,但是实际上树状数组能解决的问题线段树都可以解决。
介绍树状数组
假如我们有一个数组,要实现logn级别的区间查询,那么该怎么办呢。
我们考虑将这个数组建在一棵树上,每一个节点代表一段区间(这点在我线段树的blog里讲解了),树状数组就是由此产生的。
这就是一个树状数组(原图)
下面的是原数组,用a[i]代替,树状数组设为c[i]
那么显而易见
c [ 1 ] = a [ 1 ] c[1]=a[1] c[1]=a[1]
c [ 2 ] = a [ 1 ] + a [ 2 ] c[2]=a[1]+a[2] c[2]=a[1]+a[2]
c [ 3 ] = a [ 3 ] c[3]=a[3] c[3]=a[3]
c [ 4 ] = a [ 1 ] + a [ 2 ] + a [ 3 ] + a [ 4 ] c[4]=a[1]+a[2]+a[3]+a[4] c[4]=a[1]+a[2]+a[3]+a[4]
c [ 5 ] = a [ 5 ] c[5]=a[5] c[5]=a[5]
c [ 6 ] = a [ 5 ] + a [ 6 ] c[6]=a[5]+a[6] c[6]=a[5]+a[6]
c [ 7 ] = a [ 7 ] c[7]=a[7] c[7]=a[7]
c [ 8 ] = a [ 1 ] + a [ 2 ] + a [ 3 ] + a [ 4 ] + a [ 5 ] + a [ 6 ] + a [ 7 ] + a [ 8 ] c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8] c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]
如果转化为2进制,我们就能很快的找到树状数组组织数据的方式:
c
[
01
]
=
a
[
01
]
c[01]=a[01]
c[01]=a[01]
c
[
11
]
=
a
[
11
]
c[11]=a[11]
c[11]=a[11]
c
[
10
]
=
a
[
01
]
+
a
[
10
]
c[10]=a[01]+a[10]
c[10]=a[01]+a[10]
c
[
110
]
=
a
[
101
]
+
a
[
110
]
c[110]=a[101]+a[110]
c[110]=a[101]+a[110]
c
[
1000
]
=
a
[
0001
]
+
…
…
+
a
[
1000
]
c[1000]=a[0001]+……+a[1000]
c[1000]=a[0001]+……+a[1000]
我们发现了什么呢
假设一个数
i
i
i,转二进制后从最低位开始往高位数,一共有
k
k
k个0,那么
c
[
i
]
=
∑
i
−
2
k
+
1
i
a
[
i
]
c[i]=\sum_{i-2^k+1}^ia[i]
c[i]=∑i−2k+1ia[i]
那么看来,知道从低位到高位有多少个0是很重要的,我们不会去数0,我们选择寻找最低位的1.
lowbit
先人给了我们一个有力的公式
(lowbit返回的是
2
k
2^k
2k)
int lowbit(x){return x&(-x);}
要解释这个公式的内容,我们要先学习计算机存储负数的特性。在存储负数时,计算机会使用补码,补码就是原码取反后加一
譬如原码是正数8->00001000
补码是00001000,不变
原码是负数–8,我们先计算8的二进制00001000
那么负数的补码就是11110111+1=11111000
0的补码是00000000
x=0
如果原来的x=0,那么-x=0,按位与之后结果是0(这种情况在树状数组中并不会出现)。
x为奇数
原来的x如果是一个奇数,其二进制为????????1
(?代表未知)
那么其补码
¿¿¿¿¿¿¿0+1=¿¿¿¿¿¿¿1
(¿代表?取反,易知? &¿=0)
那么按位与后,得到的结果是00000001,就是 2 0 = 1 2^0=1 20=1
x为偶数
偶数我们分两部分讨论,第一种偶数是2的整数次幂。
则有
x
=
2
m
(
m
∈
N
)
x=2^m(m∈N)
x=2m(m∈N)
那么x可以表示为
001000000
(
m
个
0
)
001000000(m个0)
001000000(m个0)
其补码为
110111111
(
m
个
1
)
+
1
=
1100000
(
m
个
0
)
110111111(m个1)+1=1100000(m个0)
110111111(m个1)+1=1100000(m个0)
那么按位与的结果就是
0010000
(
m
个
0
)
,
2
m
0010000(m个0),2^m
0010000(m个0),2m
如果不是2的整数次幂,那么就是一个奇数乘以2的整数次幂
x
=
y
∗
2
m
x=y*2^m
x=y∗2m
相当于一个奇数左移m位,那么不难得到x&(-x)的结果依旧是lowbit。
更新与使用树状数组
单点修改与区间查询
依旧看这张图片,其中绿色的是更新过程
每一次更新a[i],都要更新与他有关的c[i],实际上就是把
i
i
i的最低位1加上一个1.
比如更新 c [ 5 ] 就 是 c [ 101 ] , 我 们 连 着 就 要 更 新 c [ 110 ] , c [ 1000 ] , c [ 10000 ] c[5]就是c[101],我们连着就要更新c[110],c[1000],c[10000] c[5]就是c[101],我们连着就要更新c[110],c[1000],c[10000]
void update(int x,int k){
while(x<=n){
tree[x]+=k;
x+=lowbit(x);
}
}
假如我们要查询区间[b,c]的值,那么我们可以用[1,c]减去[1,b-1]的值。
查询从1到某一点的值实际就是每次去掉最低位的1.
∑ i = 1 c a [ 1111 ] = c [ 1111 ] + c [ 1110 ] + c [ 1100 ] + c [ 1000 ] \sum_{i=1}^ca[1111]=c[1111]+c[1110]+c[1100]+c[1000] ∑i=1ca[1111]=c[1111]+c[1110]+c[1100]+c[1000]
int query(int x){
int ans=0;
while(x!=0){
ans+=tree[x];
x-=lowbit(x);
}
return ans;
}
到这里,你已经可以完成模板1了
区间修改,单点查询
如果我们要对一个区间内所有的数都加,如果我们一个一个修改的话比暴力还要慢,这个时候该怎么办呢
我们考虑维护原数组的差分值,如果想了解差分可以看我的另一篇文章、这样一来,我们只需要修改树状数组内的两个值,就可以方便快捷的完成修改操作。
至于单点查询,假如查询i的值,我们只需要求树状数组内[1-i]的和就可以。
模板2 Luogu P3368
这里给出模板题代码
#include<bits/stdc++.h>
#define ri register int
using namespace std;
int n,m;
long long tree[500050];
long long lowbit(int x)
{
return x&(-x);
}
void update(int pos,long long x)
{
while(pos<=n)
{
tree[pos]+=x;
pos+=lowbit(pos);
}
}
long long query(int pos) //单点询问
{
long long ans=0;
while(pos)
{
ans+=tree[pos];
pos-=lowbit(pos);
}
return ans;
}
int main()
{
scanf("%d%d",&n,&m);
long long last=0; //差分值初始化
for(ri i=1;i<=n;i++)
{
long long x;
scanf("%lld",&x);
update(i,x-last);
last=x;
}
while(m--)
{
int x;
scanf("%d",&x);
if(x==1) //区间修改
{
int l,r,k;
scanf("%d%d%d",&l,&r,&k);
update(l,k);
update(r+1,-k);
}
else
{
int y;
scanf("%d",&y);
printf("%lld\n",query(y));
}
}
}
区间修改+区间查询
我们差分后的数组求 [ 1 − i ] [1-i] [1−i]的和,实际上求得的就是a[i]的值,如果我们要求 ∑ i = l r a [ i ] \sum_{i=l}^ra[i] ∑i=lra[i]的值,该怎么办呢。
我们按照以前的思路,先求 ∑ i = 1 r \sum_{i=1}^r ∑i=1r的值,减去 ∑ i − 1 l − 1 a [ i ] \sum_{i-1}^{l-1}a[i] ∑i−1l−1a[i]就可以了
有下面的式子( d [ i ] d[i] d[i]是差分数组的值)
∑
i
=
1
r
a
[
i
]
=
∑
i
=
1
r
d
[
i
]
+
∑
i
=
1
r
−
1
d
[
i
]
+
…
…
+
∑
i
=
1
1
d
[
i
]
\sum_{i=1}^ra[i] =\sum_{i=1}^rd[i]+\sum_{i=1}^{r-1}d[i]+……+\sum_{i=1}^1d[i]
∑i=1ra[i]=∑i=1rd[i]+∑i=1r−1d[i]+……+∑i=11d[i]
=
∑
i
=
1
r
(
r
−
i
+
1
)
d
[
i
]
=\sum_{i=1}^r(r-i+1)d[i]
=∑i=1r(r−i+1)d[i]
=
n
∑
i
=
1
n
d
[
i
]
−
∑
i
=
1
n
(
i
−
1
)
∗
d
[
i
]
=n\sum_{i=1}^nd[i]-\sum_{i=1}^n(i-1)*d[i]
=n∑i=1nd[i]−∑i=1n(i−1)∗d[i]
那么我们实际上只需要维护两个前缀和,
sum1[i] = D[i],sum2[i] = D[i]*(i-1);
int n,m;
int a[50005] = {0};
int sum1[50005]; //(D[1] + D[2] + ... + D[n])
int sum2[50005]; //(1*D[1] + 2*D[2] + ... + n*D[n])
int lowbit(int x){
return x&(-x);
}
void updata(int i,int k){
int x = i; //因为x不变,所以得先保存i值
while(i <= n){
sum1[i] += k;
sum2[i] += k * (x-1);
i += lowbit(i);
}
}
int getsum(int i){ //求前缀和
int res = 0, x = i;
while(i > 0){
res += x * sum1[i] - sum2[i];
i -= lowbit(i);
}
return res;
}
int main(){
cin>>n;
for(int i = 1; i <= n; i++){
cin>>a[i];
updata(i,a[i] - a[i-1]); //输入初值的时候,也相当于更新了值
}
//[x,y]区间内加上k
updata(x,k); //A[x] - A[x-1]增加k
updata(y+1,-k); //A[y+1] - A[y]减少k
//求[x,y]区间和
int sum = getsum(y) - getsum(x-1);
return 0;
}
The end