树状数组

心得:

树状数组是一种结合了二进制的一种数据结构,区间修改的复杂度跟普通的相比n减少到logn 。
主要是两个函数:1.update区间修改函数 2.query区间询问函数
有点类似前缀和
比如a数组代表单点数据,s数组代表树状数组数据
s[4]=a1+a2+a3+a4这是和前缀和数组一样的,但是
s[6]却等于a[5]+a[6],如下图,这是因为6的二进制码为110,去掉高位1剩余一个0,它的和就为2^1=2个数的和,且为最近两个数的和,如果是s[10],二进制码为1010,去掉高位1以及夹杂的0,也是剩下一个0,所以它的和为a[9]+a[10]。

树状数组_第1张图片

函数解释:

  1. lowbit函数,个人感觉是因为发现了这个特殊的函数才有了这个数据结构,像是一个定理,然后这个数据结构相当于这个定理的应用,所以这个函数的结论需要记住:除去所有1左边的0,以及高位1,所剩下的0的个数。
  2. update函数,单点修改
    这个函数的流程就是,如果树状数组元素个数为16,那么第一次传入下标1,以及a[1],执行的是将是s[1],s[2],s[4],s[8],s[16]全部加a[1],可以类比前缀和,第二次传a[2]同理。
  3. query函数,区间查询
    树状数组本身不一定是前缀和,例如s[6]只是a[5]+a[6],结合了这个函数,求a[1]到a[6]时,结合函数,会先加上s[6],然后自减lowbit(6)=2,后变成4,所以再加上s[4]就变成了前缀和。

P3374 【模板】树状数组 1

#include 
//

using namespace std;
#define lowbit(x) ((x)&(-x))
const int N=5e5+10;

int tree[N],a[N],n,m;

//构造一个树状数组,x为下标,k为数
void update(int x,int k)
{
    while(x<=n)
    {
        tree[x]+=k;
        x+=lowbit(x);
    }
}

void build()
{
    for(int i=1;i<=n;i++)
        update(i,a[i]);
}

int query(int x)
{
    int sum=0;
    while(x)
    {
        sum+=tree[x];
        x-=lowbit(x);
    }
    return sum;
}

int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    build();
    int flag,x,y;
    for(int i=1;i<=m;i++)
    {
        scanf("%d %d %d",&flag,&x,&y);
        if(flag==1) update(x,y);
        else printf("%d\n",query(y)-query(x-1));
    }
    return 0;
}

树状数组的另一模板,区间更新,单点求值

解决的问题:区间更新时可能会超时
将树状数组的基础元素转化为差分数组,众所周知,将区间[x,y]的元素都加上k,那么它的差分数组的变化只是为cha[x]+k,cha[y+1]-k,可以证明,
我举个例子,比如1 1 1 1 1这5个数,差分数组为1 0 0 0 0,想要将[2,4]都+1,即1 2 2 2 1,差分数组变为 1 1 0 0 -1,符合变化,最后再由于树状数组本身就是求区间和,最后输出时查询单点的值,也就是相当于上一个模板中求区间和,所以可由差分数组再求出单点的值。
例题:

P3368 【模板】树状数组 2

#include 
//

using namespace std;
#define lowbit(x) ((x)&(-x))
const int N=5e5+10;

int tree[N],a[N],n,m;

//构造一个树状数组,x为下标,k为数
void update(int x,int k)
{
    while(x<=n)
    {
        tree[x]+=k;
        x+=lowbit(x);
    }
}

void build()
{
    for(int i=1;i<=n;i++)
        update(i,a[i]);
}

int query(int x)
{
    int sum=0;
    while(x)
    {
        sum+=tree[x];
        x-=lowbit(x);
    }
    return sum;
}

int main()
{
    cin>>n>>m;
    int last=0,now;
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        now=a[i];
        a[i]=now-last;
        last=now;
    }
    build();
    int flag,x,y,k;
    for(int i=1;i<=m;i++)
    {
        scanf("%d",&flag);
        if(flag==1)
        {
            scanf("%d %d %d",&x,&y,&k);
            update(x,k);
            update(y+1,-k);
        }
        else
        {
            scanf("%d",&k);
            printf("%d\n",query(k));
        }
    }
    return 0;
}

模板题三:求逆序对问题

P1908 逆序对

用到了离散化的方法
解题思路:这题求的是一个数字的之前出现的比这个数字小的和的次数,如果将数放入桶中,相当于求这个数之前的前缀和,所以可以用到树状数组来求前缀和,但是如果用桶的话,数据太大,装不下,就得用到数据离散化的方法
离散化:先用结构体标好顺序,然后再排序,然后再按顺序给数组赋值,第几大,并且数组下标得按顺序来,最后再按数组下标来进行树状数组的更新操作。

#include 
//

using namespace std;
#define lowbit(x) ((x)&(-x))
const int N=5e5+10;
//maptree;
int tree[N],n,m,rank[N];
struct node
{
    int num,val;
}a[N];

//构造一个树状数组,x为下标,k为数
void update(int x,int k)
{
    while(x<=n)
    {
        tree[x]+=k;
        x+=lowbit(x);
    }
}

int query(int x)
{
    int sum=0;
    while(x)
    {
        sum+=tree[x];
        x-=lowbit(x);
    }
    return sum;
}

bool cmp(node x,node y)
{
    if(x.val==y.val)return x.num<y.num;
    return x.val<y.val;
}

int main()
{
    cin>>n;
    long long sum=0;
    int x;
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i].val);
        a[i].num=i;
    }
    sort(a+1,a+1+n,cmp);
    for(int i=1;i<=n;i++)
      rank[a[i].num]=i;
    for(int i=1;i<=n;i++)
    {
        update(rank[i],1);
        sum+=i-query(rank[i]);
    }
    printf("%lld",sum);
    return 0;
}

你可能感兴趣的:(数据结构)