[数据结构]树状数组的基本应用

时间:2019-10-20
本文章向大家介绍[数据结构]树状数组的基本应用,主要包括[数据结构]树状数组的基本应用使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

#[数据结构]树状数组的基本应用

一、树状数组简介

这部分内容只是对树状数组的简单复习,如果您不熟悉树状数组,可以自行百度或参考其他关于树状数组的博客。

  • 树状数组是一种使用数组来模拟"树"的数据结构。树状数组的核心是lowbit(x),设p=二进制下最低位1到结尾的0的个数,那么lowbit(x)=\(2^p\)
    例:lowbit(6)=lowbit(\((110)_2\))=2;
    lowbit(8)=lowbit(\((1000)_2\))=8;

  • 我们已经知道lowbit(x)所表达的含义,下面给出计算公式:lowbit(x)=x&(-x);
    关于该公式的推导已经超出了我们讨论的范围,因此这里不再证明。
    特别要注意的是,我们在做题的时候是不能计算lowbit(0)的,即不能调用数组下标0。由于lowbit(0)=0,调用的话会陷入死循环。

  • 为什么使用lowbit(x)?为了避免查询修改两种操作出现过高的复杂度(以前缀和为例,其区间查询的复杂度很低,而单点修改的复杂度却很高),树状数组得以在O(logn)的复杂度内完成单点修改以及区间查询。

下面给出树状数组操作时的模型图:(图片摘自百度百科)
下图为查询a[1]~a[7]的权值和:
7-lowbit(7)=6;6-lowbit(6)=4;4-lowbit(4)=0;

二、树状数组的基本应用

1.区间查询、单点修改

最基础的用法。线段树可以实现树状数组的所有功能,但是树状数组的代码简短,可实施性强。因此在不必要的情况下不使用线段树。

例1: P3374【模板】树状数组1

题目描述:已知一个数列,你需要进行下面两种操作:1.将某一个数加上x; 2.求出某区间每一个数的和
思路:模板题。
代码:

#include<bits/stdc++.h>
#define N 500100
using namespace std;
int n,m,f[N];
inline int lowbit(int x){return x&(-x);}
void Update(int x,int s){
    while(x<=n){f[x]+=s;x+=lowbit(x);}
}
int Add(int x){
    int ans=0;
    while(x>0){ans+=f[x];x-=lowbit(x);}
    return ans;
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1,d;i<=n;i++){
        scanf("%d",&d);
        Update(i,d);
    }
    for(int i=1,a,x,y;i<=m;i++){
        scanf("%d%d%d",&a,&x,&y);
        if(a==1) Update(x,y);
        else printf("%d\n",Add(y)-Add(x-1));
    }
    return 0;
}

2.单点查询、区间修改

我们发现,应用二与一所解决的问题颠倒了。这样的话怎么处理呢?
我们引用一种新的方法:差分
设原数组a={1,3,2,4,8,6,7},则对应的差分数组b={1,2,-1,2,4,-2,1}
得到规律:
\[\sum_{i=1}^k b_i=a_k\]
\[b_i=a_i-a_{i-1}\]
最后的问题在于如何在差分数组上表示区间修改
如下图:

每次区间加一个值val,只会对差分数组的\(b_x\)\(b_{y+1}\)有影响。
所以我们只需要修改两个值即可:update(x,val);update(y+1,-val);

例2: P3368【模板】树状数组2

题目描述:已知一个数列,你需要进行下面两种操作:1.将某区间每一个数数加上x;2.求出某一个数的值
思路:典型的差分思想。
代码:

#include<bits/stdc++.h>
#define N 500100
using namespace std;
int n,m,f[N];
inline int lowbit(int x){return x&(-x);}
void Update(int x,int s){
    while(x<=n){f[x]+=s;x+=lowbit(x);}
}
int Sum(int x){
    int ans=0;
    while(x>0){ans+=f[x];x-=lowbit(x);}
    return ans;
}
int main()
{
    scanf("%d%d",&n,&m);
    int last=0; 
    for(int i=1,now;i<=n;i++){
        scanf("%d",&now);
        Update(i,now-last);//预处理
        last=now;
    }
    for(int i=1,a,x,y,k;i<=m;i++){
        scanf("%d",&a);
        if(a==1){
            scanf("%d%d%d",&x,&y,&k);
            Update(x,k);//差分
            Update(y+1,-k);
        }
        else{
            scanf("%d",&x);
            printf("%d\n",Sum(x));
        }
    }
    return 0;
}

3.求逆序对

例3: P1908 逆序对

题目描述:求序列中逆序对的个数
思路:我们每次读取一个值,就把这个值对应树状数组的下标加1,之后询问小于这个值的和,此时是比这个值小的数的个数。用这个值减去这个值小的数的个数就是这个数的逆序对。
代码:

#include<bits/stdc++.h>
#define ll long long
#define N 500010
using namespace std;
ll n,t[N],a[N],p[N],b[N],q[N],ans;
inline ll lowbit(ll x){
    return x&(-x);
}
inline void modify(ll x,ll k){
    while(x<=n){
        t[x]+=k;
        x+=lowbit(x);
    }
}
inline ll query(ll x){
    ll res=0;
    while(x>0){
        res+=t[x];
        x-=lowbit(x);
    }
    return res;
}
int main()
{
    scanf("%lld",&n);
    for(int i=1;i<=n;i++){
        scanf("%lld",&a[i]);
        b[i]=a[i];
    }
    sort(b+1,b+n+1);
    int m=unique(b+1,b+n+1)-b-1;
    for(int i=1;i<=n;i++){
        int pos=lower_bound(b+1,b+m+1,a[i])-b;
        q[i]=pos;
    }//离散化处理
    for(int i=1;i<=n;i++){
        modify(q[i],1);//统计逆序对个数
        ans+=i-query(q[i]);
    }
    printf("%lld",ans);
    return 0;
}

例4: UVA10810 Ultra-QuickSort

题目描述:使一个序列有小到大排列的两个数最小交换次数
思路:只有\(a_i>a_j\)是才会交换,故转化为逆序对处理。
代码:

#include<bits/stdc++.h>
#define N 500010
using namespace std;
int a[N],tree[N],maxpos,n;
inline int lowbit(int x){return x&(-x);}
inline void modify(int x,int k){
    while(x<=maxpos){
        tree[x]+=k;
        x+=lowbit(x);
    }
}
inline int query(int x){
    int res=0;
    while(x>0){
        res+=tree[x];
        x-=lowbit(x);
    }
    return res;
}
int main()
{
    while(scanf("%d",&n)&&n){
        maxpos=0;
        memset(tree,0,sizeof(tree));
        for(int i=1;i<=n;i++){
            scanf("%d",&a[i]);
            a[i]++;
            maxpos=max(maxpos,a[i]);
        }
        int ans=0;
        for(int i=n;i>=1;i--){
            ans+=query(a[i]-1);
            modify(a[i],1);
        }
        printf("%d\n",ans);
    }
    return 0;
}

例5: P1774 最接近神的人_NOI导刊2010提高(02)

思路:一道双倍经验题。例三完全一致。

原文地址:https://www.cnblogs.com/cyanigence-oi/p/11708420.html