树状数组详解及其应用
前言:
之前以为树状数组搞懂了,结果一个树状数组题把我打回原形,原来我只是会背模板而已。。前来复习 学习一遍
为什么用树状数组
树状数组是一种存储数据的结构,通过压缩数据来达到高效的查询效率
在一些题目中需要多次查询区间和,同时更改点,如果我们暴力去更改和查询复杂度是O(n),如果用前缀和实现,虽然查询快但是更改是On,但是用树状数组维护,查询和更改都是O(logn),当然不只是单点修改+区间询问,在区间修改+区间询问,单点修改+区间最大值,逆序对查询都有很好的表现
怎么实现树状数组
树状数组怎么形成(有什么规律)
首先,树状数组和树形结构有什么关系呢
这是一颗树
这是树状数组
可以发现树状数组其实就是树形结构整体向右拉,树状数组中每个点包含着一个或者多个值相加的结果
那么我们把树状数组和原数组的对应关系再写详细一些,看看其中的规律
C1 = A1
C2 = C1 + A2 = A1 + A2
C3 = A3
C4 = C2 + C3 + A4 = A1 + A2 + A3 + A4
C5 = A5
C6 = C5 + A6 = A5 + A6
C7 = A7
C8 = C4 + C6 + C7 + A8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8
可能现在看不出来规律,那我们把它写成二进制看看
观察树状数组中最后一位1的位置,可以发现,每个树状数组包含的原数组的个数等于树状数组二进制最后一位1和后面的数组成的数
比如6(110)最后一位1形成的数为2(10),所以C6 包含两项
因此总结出公式
C[i] = A[i - 2^k+1] + A[i - 2^k+2] + … + A[i];
lowbit函数(形成树状数组的关键)
刚刚所说的二进制最后一位1和后面的数组成的数,这么抽象,到底要怎么写呢
其实很简单,有两种写法
int lowbit1(int x) return x&(x^(x-1);
int lowbit2(int x) return x&-x;
这里两种写法都可以,首选第二种,因为更短,第二种巧妙利用了负数是补码的特性,
那么有了lowbi函数我们就可以轻松知道树状数组中下标为 i 包含原数组就是
[ i - lowbit(i), i ]
实际应用
单点修改 + 区间和询问
区间询问
这是树状数组中最为经典的用法
我们已经知道树状数组中下标为 i 包含原数组就是[ i - lowbit(i), i ],那么我们想要求出原数组[1,i]前缀和,那每次我们都计算[ i - lowbit(i), i ],然后让i减去它所包含的项目个数,也就是 i -= lowbit(i),直到出现i等于0,就可能把1到i的前缀和算出来
int getsum(int ix){ //求A[1 - ix]的和
int res = 0;
for(int i = ix;i>0;i-=lowbit(i)){
res += tree[i];
}
return res;
}
单点修改
由于在树状数组,每个值包含的不是原数组了,当修改单点时会影响后面多个树状数组值,当你修改了i的值时,后面包含该值的所有数都会变化
因为 A[i] 包含于 C[i + 2^k]、C[(i + 2^k) + 2^k],也就是你每次修改的下标都是i+lowbit(i)
void update(int ix,int x){
for(int i = ix;i<=n;i+=lowbit(i){
tree[i] += x;
}
}
所以单点修改 + 区间和询问的代码为
int A[100005],tree[400005]
int lowbit(int x) return x&-x;
void update(int ix,int x){ //在ix位置加上x值
for(int i = ix;i<=n;i+=lowbit(i){
tree[i] += x;
}
}
int getsum(int ix){ //求A[1 - ix]的和
int res = 0;
for(int i = ix;i>0;i-=lowbit(i)){
res += tree[i];
}
return res;
}
区间修改 + 区间询问
可能你会问 区间修改 + 单点询问 呢?用两次区间询问相减就是单点询问了
但是如果我们只有一次单点询问,我们有更常用的方法那就是差分数组,有了这个启发,很容易去想用树状数组去实现差分数组
所以解决区间修改 + 区间询问就是差分树状数组
区间修改
对于普通的差分数组修改来说 [l,r]增加x 只需要 A[l] += x , A[r] += x,即可,那对于树状数组来说其实是一样的,树状数组就是数据结构,本质上还是个数组
void add(ll t[],ll ix,ll x){
for(int i = ix;i<=n;i+=(i&-i)){
t[i] += x;
}
}
cin>>c;
add(tree, b + 1,-c);
add(tr,b+1,(b + 1)*-c);
区间询问
压力就给到了区间询问这边
在差分数组中我们查询单点时,需要计算前缀和,那区间询问我们就要计算多次前缀和吗? 显然不用,我们来看看规律
A1 = C1
A2 = C1 + C2
A3 = C1 + C2 + C3
A4 = C1 + C2 + C3 + C4
因此我们有 A n = ∑ i = 1 n C i A_n = \sum_{i=1}^n C_i An=∑i=1nCi
设Sn 是 区间和 ,那么
S1 = A1
S2 = A1 + A2
S3 = A1 + A2 + A3
S4 = A1 + A2 + A3 + A4
因此我们有
S
n
=
∑
i
=
1
n
A
i
S_n = \sum_{i=1}^n A_i
Sn=∑i=1nAi
结合起来
S1 = C1
S2 = 2C1 + C2
S3 = 3C1 + 2C2 + C3
S4 = 4C1 + 3C2 + 2C3 + C4
因此我们有
S
n
=
∑
i
=
1
n
∑
j
=
1
i
A
j
=
∑
i
=
1
n
(
n
−
i
+
1
)
C
i
=
∑
i
=
1
n
(
n
+
1
)
C
i
−
∑
i
=
1
n
i
C
i
S_n = \sum_{i=1}^n \sum_{j=1}^i A_j =\sum_{i=1}^n(n-i+1)C_i=\sum_{i=1}^n(n+1)Ci-\sum_{i=1}^niC_i
Sn=i=1∑nj=1∑iAj=i=1∑n(n−i+1)Ci=i=1∑n(n+1)Ci−i=1∑niCi
所以我们计算区间和要用两个树状数组,一个记录 i*Ci的前缀和一个记录Ci的前缀和
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+7;
typedef long long ll;
ll tree[N],tr[N],n;
void add(ll t[],ll ix,ll x){
for(int i = ix;i<=n;i+=(i&-i)){
t[i] += x;
}
}
ll ask(ll t[],ll ix){
ll res = 0;
for(int i = ix;i>0;i-=(i&-i)){
res += t[i];
}
return res;
}
ll A[N],m;
ll sum(ll ix){
return (ix+1)*ask(tree,ix)-ask(tr,ix);
}
int main(){
cin>>n>>m;
int pre = 0;
for(int i = 1;i<=n;i++){
cin>>A[i];
add(tree,i,A[i]-A[i-1]);
add(tr,i,i*(A[i]-A[i-1]));
}
while(m--){
char ch;
int a,b,c;
cin>>ch>>a>>b;
if(ch=='Q'){
cout<<sum(b)-sum(a-1)<<endl;
}
else {
cin>>c;
add(tree,a,c), add(tree, b + 1,-c);
add(tr,a,a*c), add(tr,b+1,(b + 1)*-c);
}
}
return 0;
}
区间最大值 /最小值
如果你已经了解树状数组的形成,那么对于区间最大值和区间和来说是差不多的,只不过一个取最大值,一个是累加
区间修改
对于区间修改,考虑修改一个点对于后面的最大值影响,这和区间和的想法基本一致
void update(int ix ,ll x){
for(int i = ix;i<=n;i+=(i&-i))
tree[i] = max(x,tree[i]);
}
区间查询最大值
对于区间查询我们不能像查询区间和一样用两次前缀和相减解决,因为区间最大值只跟区间内的元素有关
所以我们要回到树状数组定义中,直接在区间内找最大值
那如何查询
由于C【i】 是包含 [i-lowbit(i),i ]的最大值的,如果l 大于 i - lowbit 时意味着查询区间时不合法的,所以我们不能直接拿C【i】取最大值,我们只能 取原数组A【i】的最大值,然后 查询i-1的位置,直到到达l。
例如此时我们查询(3,6)的最大值,对于C【6】包含[5,6 ]的最大值,是大于3的,是合法的,C【6】可能最大值,然后 6 - lowbit(6) ,也就是查询 [3,4] , 此时C【4】包含[1,4] 显然是不合法的,只能取原数组A[4]的最大值,然后查询[3,3] ,然后包含[3,3]合法,就取C【3】的最大值
所以查询(3,6)的最大值 = max{C[6],A[4],C[3]}
ll ask(int l,int r){
ll ans=A[r];
while(l<=r){
for(;r-(r&-r)+1>=l&&r>=l;r-=(r&-r)){
ans = max(tree[r],ans);
}
if(l<=r)ans = max(A[r],ans);
r--;
}
return ans;
}
总代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+7;
typedef long long ll;
ll tree[N],A[N];
ll n;
void update(int ix ,ll x){
for(ll i = ix;i<=n;i+=(i&-i))
tree[i] = max(x,tree[i]);
}
ll ask(int l,int r){
ll ans=A[r];
while(l<=r){
for(;r-(r&-r)+1>=l&&r>=l;r-=(r&-r)){
ans = max(tree[r],ans);
}
if(l<=r)ans = max(A[r],ans);
r--;
}
return ans;
}
int main(){
int T;
cin>>n;
memset(tree,-0x3f,sizeof(tree));
for(int i = 1;i<=n;i++){
cin>>A[i];
update(i,A[i]);
}
cin>>T;
while(T--){
int b,c;
cin>>b>>c;
cout<<ask(b,c)<<endl;
}
return 0;
}