数据结构—树状数组

树状数组

  • 单点修改、区间查询
  • 区间修改、单点查询
  • 区间修改、区间查询

单点修改、区间查询

这里讲解树状数组的最基本操作单点修改、区间查询,当然能做到单点修改、区间查询,肯定就能做到单点修改、单点查询了。树状数组是用来快速求前缀和的,传统的单点修改、区间查询要么单点修改的复杂度是 O ( n ) O(n) O(n)查询是 O ( 1 ) O(1) O(1),要么查询复杂度是 O ( n ) O(n) O(n)修改是 O ( 1 ) O(1) O(1),树状数组相当于一个比较综合的算法,树状数组的查询时间复杂度和修改时间复杂度都是 O ( l o g n ) O(log n) O(logn)的。

下面这张图基本上是讲解树状数组必用的一张图
数据结构—树状数组_第1张图片
树状数组初学起来还是比较难以理解,过弄明白之后代码就很好写了,树状数组相当于是拿空间换时间,原数组为a[],额外数组为c[],先来讲一下lowbit操作

lowbit(x)返回x的最后一个1及其后面的0(可能不存在)构成的数字
比如
lowbit(3)=1,	3的二进制是11	,最后一位1及其后面的01
lowbit(4)=4,	4的二进制是100	,最后一位1及其后面的0100
lowbit(6)=2, 4的二进制是110	,最后一位1及其后面的010
lowbit函数也很好实现
lowbit(x)=x&-x
因为计算机是存的补码,-x相当于x取反再加1,例如x=10010,-x=1110
-x&x=10

树状数组主要有两个操作:

1.某一位置的数+x 
2.求区间[1,r]的和([l,r]=[1,r]-[1,l-1]

观察上图可以发现

c[1]=a[1]
c[2]=c[1]+a[2]
c[3]=a[3]
c[4]=c[2]+c[3]+a[4]
...
c[16]=c[8]+c[12]+c[14]+c[15]+a[16]

假设我们已经维护好了c[]数组,那么我们如何求前缀和呢
比如求a[1~15]的和
我们可以发现

15的二进制为1111=1000+100+10+1=8+4+2+1
sum[1,15]=c[15]+c[14]+c[12]+c[8]=c[15-0]+c[15-0-1]+c[15-0-1-2]+c[15-0-1-2]+c[15-0-1-2-4]
有没有发现有什么关系
其中
lowbit(1111)=1,1111-1=1110
lowbit(1110)=2,1110-2=1100
lowbit(1100)=4,1100-4=1000
lowbit(1000)=8,1000-8=0

所以根据上面的推导就能写出查询函数query(r),查询[1,x]的前缀和

int query(int r){
    int res=0;
    for(int i=r;i>=0;i-=lowbit(i)) res+=f[i];
    return res;
}

这是查询操作,我们还需要修改操作,每次修改a[](将a[i]变成b,就相当于a[i]+b-a[i],更改操作可以转换成+操作)都需要更改其对应的c[]数组的值

a[2]对应的c[]数组有
c[2]、c[4]、c[8]、c[16],对应的二进制下标为
 10   100  1000  10000
其中10=2
4=100=2+lowbit(2)
8=1000=4+lowbit(4)
16=10000=8+lowbit(8)

所以某一位置的数+x 的函数为

void add(int x,int c){
    for(int i=x;i<=n;i+=lowbit(i))  f[i]+=c;//这里f数组是上图中的c数组,c是要加的值x
}

至于为什么操作可以看b站这两个视频 五分钟丝滑动画讲解 | 树状数组、 〔manim | 算法 | 数据结构〕 完全理解并深入应用树状数组
有一道树状数组的最经典例题可以做一下,AcWing1264. 动态求连续区间和
代码如下:

#include
#include
#include
#include
using namespace std;
const int N=1e5+10;

int f[N];
int n,m;
int lowbit(int x){
    return x&-x;
}
void add(int x,int c){
    for(int i=x;i<=n;i+=lowbit(i))  f[i]+=c;
}
int query(int r){
    int res=0;
    for(int i=r;i>=1;i-=lowbit(i)) res+=f[i];
    return res;
}

int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        int x;
        scanf("%d",&x);
        add(i,x);
    }
    while(m--){
        int k,l,r;
        scanf("%d%d%d",&k,&l,&r);
        if(k==0) printf("%d\n",query(r)-query(l-1));
        else add(l,r);
    }
    return 0;
}

还有两道例题:
AcWing1265. 数星星
AcWing 241. 楼兰图腾

区间修改、单点查询

之前的单点修改区间查询,维护的是原数组的树状数组,这里我们维护差分数组的树状数组即可,可看例题 AcWing 242. 一个简单的整数问题
代码如下

#include
#include
#include
#include
using namespace std;
const int N=1e5+10;

int n,m;
int c[N];

int lowbit(int x){
    return x&-x;
}
void add(int idx,int x){
    for(int i=idx;i<=n;i+=lowbit(i)) c[i]+=x;
}
int query(int r){
    int res=0;
    for(int i=r;i;i-=lowbit(i)) res+=c[i];
    return res;
}
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        int x;
        scanf("%d",&x);
        add(i,x);
        add(i+1,-x);
    }
    while(m--){
        char op[2];
        cin>>op;
        if(*op=='Q'){
            int r;
            cin>>r;
            cout<<query(r)<<endl;
        }
        else{
            int l,r,d;
            cin>>l>>r>>d;
            add(l,d);
            add(r+1,-d);
        }
    }
    return 0;
}

区间修改、区间查询

区间修改,区间查询较为复杂一些,要维护两个树状数组
首先有差分数组b[]原数组f[]

首先初始化有

b[1]=f[1]
b[2]=f[2]-f[1]
b[3]=f[3]-f[2]
......
b[n]=f[n]-f[n-1]

我们要区间修改,区间查询,区间修改可以通过差分来实现,区间查询的推导如下
要求区间[1,x]的和,就是求 ∑ i = 1 x f [ i ] \sum\limits_{i=1}^{x}f[i] i=1xf[i] ,其中 f [ i ] = ∑ j = 1 i b [ j ] f[i]=\sum\limits_{j=1}^{i}b[j] f[i]=j=1ib[j] ∑ i = 1 x f [ i ] = ∑ i = 1 x ∑ j = 1 i b [ j ] \sum\limits_{i=1}^{x}f[i]=\sum\limits_{i=1}^{x}\sum\limits_{j=1}^{i}b[j] i=1xf[i]=i=1xj=1ib[j]为了方便看,我们可以写成下面这种形式

f[1]= b[1]
f[2]= b[1]+b[2]
f[3]= b[1]+b[2]+b[3]
.....
f[x]= b[1]+b[2]+b[3]+.....+b[x]

右侧的这部分像矩阵,我们把这个矩阵补全

\b[1]+b[2]+b[3]+b[4]+.....+b[x]
b[1]\+b[2]+b[3]+b[4]+.....+b[x]
b[1]+b[2]\+b[3]+b[4]+.....+b[x]
b[1]+b[2]+b[3]\b[4]+.....+b[x]
.....				    \+b[x]	
b[1]+b[2]+b[3]+.....     +b[x]

这个矩阵的值为 ( x + 1 ) ∗ ( b [ 1 ] + b [ 2 ] + . . . . + b [ x ] ) (x+1)*(b[1]+b[2]+....+b[x]) (x+1)(b[1]+b[2]+....+b[x]),我们补全这个矩阵用到的数的和为 1 ∗ b [ 1 ] + 2 ∗ b [ 2 ] + . . . + x ∗ b [ x ] 1*b[1]+2*b[2]+...+x*b[x] 1b[1]+2b[2]+...+xb[x]
所以 ∑ i = 1 x f [ i ] = ∑ i = 1 x ∑ j = 1 i b [ j ] \sum\limits_{i=1}^{x}f[i]=\sum\limits_{i=1}^{x}\sum\limits_{j=1}^{i}b[j] i=1xf[i]=i=1xj=1ib[j]= ( x + 1 ) ∗ ( b [ 1 ] + b [ 2 ] + . . . . + b [ x ] ) − ( 1 ∗ b [ 1 ] + 2 ∗ b [ 2 ] + . . . + x ∗ b [ x ] ) (x+1)*(b[1]+b[2]+....+b[x])-(1*b[1]+2*b[2]+...+x*b[x]) (x+1)(b[1]+b[2]+....+b[x])(1b[1]+2b[2]+...+xb[x])
我们需要两个树状数组c1[]、c2[],c1是b数组的树状数组,c2是i*b[i]的树状数组
树状数组主要有两个操作,查询操作,这两个数组的操作是一样的,对于修改操作,例如在[l,r]区间内加上一个数h
对于c1数组的修改操作为

add(c1,l,h)
add(c1,r+1,-h)

对于c2数组的修改操作为

add(c2,l,l*h)
add(c2,r+1,-(r+1)*h)

例题: AcWing 243. 一个简单的整数问题2
代码如下

#include
#include
#include
#include
typedef long long ll;
using namespace std;
const int N=1e5+10;

ll c1[N],c2[N];
int f[N];
int n,m;

int lowbit(int x){
    return x&-x;
}
void add(ll c[],int idx,ll x){
    for(int i=idx;i<=n;i+=lowbit(i)) c[i]+=x;
}
ll query(ll c[],int r){
    ll res=0;
    for(int i=r;i;i-=lowbit(i)) res+=c[i];
    return res;
}

int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++) scanf("%d",&f[i]);
    for(int i=1;i<=n;i++){
        add(c1,i,f[i]-f[i-1]);
        add(c2,i,(ll)(f[i]-f[i-1])*i);
    }
    while(m--){
        char op[2];
        int l,r,d;
        cin>>op;
        if(*op=='C'){
            cin>>l>>r>>d;
            add(c1,l,d);
            add(c1,r+1,-d);
            add(c2,l,(ll)l*d);
            add(c2,r+1,-(r+1)*(ll)d);
        }
        else{
            cin>>l>>r;
            cout<<(ll)(r+1)*query(c1,r)-(ll)query(c2,r)-(ll)(l)*query(c1,l-1)+(ll)query(c2,l-1)<<endl;
        }
    }
}

你可能感兴趣的:(树状数组,线段树,数据结构,前缀和,算法,数据结构,c++)