树状数组学习笔记

众所周知,树状数组是一个常用的数据结构。。。

1.为啥用树状数组:

如果用普通的前缀数组来维护前缀的信息,即使查询时o(1)的,但是修改就几乎要o(n),效率有时十分低下.

而树状数组却弥补了这一缺点,修改和查询都是o(logn)的

2.如何构建树状数组:

根据二次幂的性质,我们可以把一个数转化成一个独一无二的二进制数,所以,我们可以建立一个类似于二进制数的数组来维护前缀和

假如一个整数  x可以别分为x=2^i1+2^i2+2^i3...+2^im那么就可以把一个区间[1...x]分为(logx)的几个小区间

假设i1>i2>i3>...im

1长度为2^i1区间[1,2^i1]

2长度为2^i2区间[2^i1+1,2^i1+2^i2]

3长度为2^i3区间[2^i2+2^i1+1,2^i1+2^i2+2^i3]

...

这些小区间的特点是长度为二进制分解下最小的二次幂,也就是lowbit(x);

 例如11=8+2+1=2^3+2^1+2^0,那么区间11可以分为[1,8],[9,10],[11],长度分别为lowbit(8)=8,lowbit(10)=2,lowbit(11)=1

 

 C[i]=A[i]+A[i-1]+A]i-lowbit(i)+1]

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[6] = A[5] + A[6];

C[7] = A[7];

C[8] = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8];

求lowbit(i):

lowbit(i)=i&(-i);

修改

void insert(long long x,long long vol) {
    while(x<=n) {
        c[x]+=vol;
        x+=lowbit(x);
    }
}

查询

long long ask(long long x) {
    long long sum=0;
    while(x) {
        sum+=c[x];
        x-=lowbit(x);
    }
    return sum;
}

特别注意,树状数组的下标不能为0,比如insert(0,a)和ask(0)

模板题https://www.luogu.com.cn/problem/P3374

话不多说,直接上代码

#include<bits/stdc++.h>
using namespace std;
long long lowbit(long long x) {
    return x&(-x);
}
long long n,m,c[1000000],a[1000000];
void insert(long long x,long long vol) {
    while(x<=n) {
        c[x]+=vol;
        x+=lowbit(x);
    }
}
long long ask(long long x) {
    long long sum=0;
    while(x) {
        sum+=c[x];
        x-=lowbit(x);
    }
    return sum;
}

int main() {
    scanf("%lld%lld",&n,&m);
    for(long long i=1; i<=n; i++) {
        scanf("%lld",&a[i]);
        insert(i,a[i]);
    }

    for(long long i=1; i<=m; i++) {
        long long x,y,s;
        scanf("%lld%lld%lld",&s,&x,&y);
        if(s==1) insert(x,y);
        else {
            long long p=ask(y)-ask(x-1);
            printf("%lld\n",p);
        }
    }

}

 

 例题:https://www.luogu.com.cn/problem/P1428

 第一眼看到题目,就想到了暴力,发现就是求维护一个小于第i头鱼的可爱值的前缀,但如果数据范围大一些呢?

仔细分析,这里我们就需要运用到树状数组

样例:

6
4 3 0 5 1 2
我们可以把每一个小鱼的权值当作当作树状数组的序号,把它的权值加上一,就相当于等于这个权值的小鱼多了一个,我们只需要找出小于这个权值的数目就行了
#include<bits/stdc++.h>
using namespace std;
const int N=100001;
int c[N],n,ans,b[N];
int lowbit(int x)
{
    return x&(-x);
}
struct data{
    int x,y;
}a[N];
bool cmp(data c,data d){
    if(c.x==d.x) return c.y<d.y;
    return c.x<d.x;
}
void insert(int x)
{
    while(x<=N)
    {
        c[x]++;//为什么是加1呢就相当于是把等于这个权值的小鱼加1 
        x+=lowbit(x);
    }
}
int ask(int x)
{
    long long sum=0;
    while(x)
    {
        sum+=c[x];
        x-=lowbit(x);
    }
    return sum;
}
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        int x;
        scanf("%d",&x);
            cout<<ask(x)<<" ";//找出比x小的有多少 
        insert(x+1);//因为是统计小于这个数的,所以要加1,请仔细思考 
    }

}

如果数据太大,要用离散化

#include<bits/stdc++.h>
using namespace std;
const int N=100001;
int c[N],n,ans,b[N];
int lowbit(int x)
{
    return x&(-x);
}
struct data{
    int x,y;
}a[N];
bool cmp(data c,data d){
    if(c.x==d.x) return c.y>d.y; //这样可以保证严格小于的情况 
    return c.x<d.x;
}
void insert(int x)
{
    while(x<=n)
    {
        c[x]++;
        x+=lowbit(x);
    }
}
int ask(int x)
{
    long long sum=0;
    while(x)
    {
        sum+=c[x];
        x-=lowbit(x);
    }
    return sum;
}
int main(){
    scanf("%d",&n);
    a[0].x=-1;
    for(int i=1;i<=n;i++){
       scanf("%d",&a[i].x);
       a[i].y=i;//记录第i个数的坐标,因为在排序后会被打乱 
    }
    sort(a+1,a+n+1,cmp);
    int tot=1;
    for(int i=1;i<=n;i++){    
    b[a[i].y]=i; //a[i].y相当于第i小的数的坐标,把这个坐标设为第i小 
}

    for(int i=1;i<=n;i++){
    cout<<ask(b[i])<<" ";//b[i]相当于第i个数是第几大的 
        insert(b[i]);
    }
}

但由于要排序,所以这个代码不快

例题2:http://bzoj.org/p/1016

首先,可以写dp

转移方程轻易得出

设s[i]是前i个数的前缀

f[i]+=f[j](s[j]>s[i])

#include<bits/stdc++.h>
using namespace std;
const long long N=5*1e5;
const long long mod=1000000009;
long long s[N],ans,n,a[N],f[N];
int main(){
    scanf("%lld",&n);
    for(long long i=1;i<=n;i++)
    scanf("%lld",&a[i]),s[i]=s[i-1]+a[i];
    f[0]=1;
    for(int i=1;i<=n;i++)
    for(int j=0;j<i;j++)
    {
        if(s[i]>=s[j])
        f[i]=(f[i]+f[j])%mod;
    }
        cout<<f[n];
}

 这一段其实可以用树状数组来优化

只需要找到前面有多少比他小

f[0]=1,所以当s[i]<0,那么这种情况就不可取,所以也要把0打入树状数组

#include<bits/stdc++.h>
using namespace std;
const long long N=1e6+10,mod=1000000009;
long long n,c[N],aa[N],ans;
struct data{
    long long num,vol;
}a[N];
bool cmp(data c,data d){
    if(c.vol==d.vol) return c.num<d.num;
    else return c.vol<d.vol;
}
long long lowbit(long long x){
    return x&(-x);
}
void insert(long long x,long long vol){
    while(x<=N){
        c[x]=(c[x]+vol)%mod;
        x+=lowbit(x);
        
    }
}
long long ask(long long x){
    long long sum=0;
    while(x){
        sum=(sum+c[x])%mod;
        x-=lowbit(x);
    }
    return sum;
}
int main(){
    scanf("%lld",&n);    
    for(long long i=1;i<=n;i++)
    {
        long long x;
        scanf("%lld",&x);
        a[i].vol=a[i-1].vol+x;//算出前缀    
        a[i].num=i;    //标记位置,日常离散化 
    }
    sort(a+1,a+n+1,cmp);
    long long falg=0;//falg统计有多少个前缀比0小 (falg比flag好打,篡改单词) 
    for(int i=1;i<=n;i++){
    if(a[i].vol<0) falg++; 
}
    falg++;//falg统计有多少个前缀比0小 ,所以0应该是第falg+1小 
    long long tot=0,i=0;//tot表示运行了多少次,应该要运行n+1次,因为还要包括0,i表示第i小的数 
    while(tot<=n+1){
    
    if(tot==falg) insert(falg,1);//为什么这里i不++呢,因为没有跳到下一个值,insert(falg,1)相当于把0放入树状数组,因为0是第flag小的 
    else aa[a[i].num]=tot,i++;
    tot++;
}//日常离散化  

    for(long long i=1;i<=n;i++)
    {  
       long long f=ask(aa[i]);
       if(i==n) cout<<f;//输出f[n] 
       insert(aa[i],f);
    }

}

树状数组虽然快但是也有局限性,维护的东西特别少,不宜拓展

谢谢阅读

//黄鸡djskal爆蔡我

 

posted @ 2020-07-03 21:25  CJXYY  阅读(249)  评论(0编辑  收藏  举报