树状数组应用总结

前言

做个总结,忘记之后再翻翻

首先明确一下树状数组的结构性质:

1.每个内部节点 c [ x ] c[x] c[x]保存以它为根的子树中所有叶节点的和

2.每个内部节点 c [ x ] c[x] c[x]的子节点个数等于 l o w b i t ( x ) lowbit(x) lowbit(x)的位数,即位为1的数量

例如:
x = 9 = 2 3 + 2 1 x=9=2^3+2^1 x=9=23+21,分为 [ 1 , 8 ] , [ 9 , 9 ] [1,8],[9,9] [1,8],[9,9]两个区间,区间和分别为 c [ 8 ] , c [ 9 ] c[8],c[9] c[8],c[9]
x = 13 = 2 3 + 2 2 + 2 0 x=13=2^3+2^2+2^0 x=13=23+22+20,分为 [ 1 , 8 ] , [ 9 , 12 ] , [ 13 , 13 ] [1,8],[9,12],[13,13] [1,8],[9,12],[13,13]三个区间,区间和分别为 c [ 8 ] , c [ 12 ] , c [ 13 ] c[8],c[12],c[13] c[8],c[12],c[13]

3.除树根外,每个内部节点 c [ x ] c[x] c[x]的父节点是 c [ x + l o w b i t ( x ) ] c[x+lowbit(x)] c[x+lowbit(x)]

4.树的深度为 O ( l o g N ) O(logN) O(logN)。因此可以保证在 O ( l o g N ) O(logN) O(logN)的时间内查询前缀和

在这里插入图片描述



树状数组应用总结

主要分为三种:

  • 单点修改,单点查询(最基础)
  • 区间修改,单点查询
  • 区间修改,区间查询

应用一:单点修改,单点查询

最基础的定义

查询
int ask(int x)
{
    int ans=0;
    for (;x;x-=x&-x)ans+=c[x];
    return ans;
}
修改
void add(int x,int y)
{
    for (;x<=n;x+=x&-x)c[x]+=y;
}

例题:POJ2182
题意:有 n n n头奶牛,身高 1 − n 1-n 1n不等,站成一列,已知第 i i i头奶牛前面有 A i A_i Ai头比他矮,求出所有身高。
思路:从后往前思考。 A n A_n An表示前 n − 1 n-1 n1头牛有 A n A_n An头比最后一头矮,由于不存在重复身高,最后一头牛的身高肯定是排第 A n + 1 A_n+1 An+1位的,同理第 i i i头的身高一定是排第 A i + 1 A_i+1 Ai+1位,但是已经出现过的身高就得排除。可以通过维护一个初始全1的01序列,查询第 k k k个1的位置并且将其修改为0。用二分+树状数组即可
代码

int n,A[maxn],c[maxn],ans[maxn];
int ask(int x){int ans=0;for(;x;x-=x&-x)ans+=c[x];return ans;}
void add(int x,int y){for(;x<=n;x+=x&-x)c[x]+=y;}
int solve(int x)
{
    int l=1,r=n;
    while(l<r)
    {
        int mid=(l+r)>>1;
        if (ask(mid)<x)l=mid+1;
        else r=mid;
    }
    return l;
}
int main()
{
    scanf("%d",&n);
    A[1]=0;rep(i,2,n)scanf("%d",&A[i]);
    rep(i,1,n)add(i,1);
    for (int i=n;i>=1;i--)
    {
        ans[i]=solve(A[i]+1);
        add(ans[i],-1);
    }
    rep(i,1,n)W(ans[i]);
}


应用二:区间修改,单点查询

新建数组 b b b,前缀和 b [ 1 b[1 b[1~ x ] x] x]反映了指令"C l r d"对 a [ x ] a[x] a[x]的影响
1.把 b [ l ] + d b[l]+d b[l]+d
2.把 b [ r + 1 ] − d b[r+1]-d b[r+1]d
观察一下数组 b b b的变化:
1.当 1 ≤ x < l 1\leq x< l 1x<l时:前缀和 b [ 1 b[1 b[1~ x ] x] x]无变化
2.当 l ≤ x ≤ r l \leq x \leq r lxr时:前缀和 b [ 1 b[1 b[1~ x ] x] x]增加了 d d d,因为 b [ l ] b[l] b[l]增加了 d d d
3.当 r < x ≤ n r< x \leq n r<xn时:前缀和 b [ 1 b[1 b[1~ x ] x] x],因为一加一减刚好抵消

也就是说 对于区间 [ l , r ] [l,r] [l,r]内的数的影响,都体现在了数组 b b b的前缀和中
而求前缀和就是树状数组的基础操作,累加上原先的 a [ x ] a[x] a[x]即可得到"Q x"的答案

蓝书例题:
代码

int n,m,l,r,x,a[maxn],c[maxn];
char s[5];
int ask(int x){int ans=0;for (;x;x-=x&-x)ans+=c[x];return ans;}
void add(int x,int y){for (;x<=n;x+=x&-x)c[x]+=y;}
int main()
{
    scanf("%d%d",&n,&m);
    rep(i,1,n)scanf("%d",&a[i]);
    while(m--)
    {
        scanf("%s",s);
        if (s[0]=='Q')
        {
            scanf("%d",&x);
            W(ask(x)+a[x]);
        }
        else if (s[0]=='C')
        {
            scanf("%d%d%d",&l,&r,&x);
            add(l,x);
            add(r+1,-x);
        }
    }
}


应用三:区间修改,区间查询

首先仍是维护一个数组 b b b a [ x ] a[x] a[x]变化量就是前缀和 b [ 1 b[1 b[1~ x ] x] x]
那么原数组 a a a的前缀和 a [ 1 a[1 a[1~ x ] x] x]变化量就是:
∑ i = 1 x ∑ j = 1 i b [ j ] = ( x + 1 ) ∑ i = 1 x b [ i ] − ∑ i = 1 x ( i × b [ i ] ) \sum_{i=1}^x\sum_{j=1}^i b[j]=(x+1)\sum_{i=1}^x b[i]-\sum_{i=1}^x (i×b[i]) i=1xj=1ib[j]=(x+1)i=1xb[i]i=1x(i×b[i])
第一部分就是维护 b b b的前缀和
第二部分就是维护 i × b [ i ] i×b[i] i×b[i]的前缀和,令 i × b [ i ] = c 2 [ x ] i×b[i]=c2[x] i×b[i]=c2[x]
由于只是乘了个 i i i,故与之前对应的操作是:
1.把 c 2 [ l ] + l d c2[l]+ld c2[l]+ld
2.把 c 2 [ r + 1 ] − ( r + 1 ) d c2[r+1]-(r+1)d c2[r+1](r+1)d
观察一下数组 b b b的变化:
l ≤ x ≤ r l \leq x \leq r lxr时:前缀和 c 2 [ 1 c2[1 c2[1~ x ] x] x]增加了 l d ld ld,因为 c 2 [ l ] c2[l] c2[l]增加了 l d ld ld

综上所述: a a a的原始前缀和+ a a a的前缀和变化量即为最终的 a a a的前缀和
S u m [ r ] = s u m [ r ] + ( r + 1 ) ∑ i = 1 r b [ i ] − ∑ i = 1 r ( i × b [ i ] ) Sum[r]=sum[r]+(r+1)\sum_{i=1}^r b[i]-\sum_{i=1}^r (i×b[i]) Sum[r]=sum[r]+(r+1)i=1rb[i]i=1r(i×b[i])
= s u m [ r ] + ( r + 1 ) a s k 1 ( r ) − a s k 2 ( r ) =sum[r]+(r+1)ask1(r)-ask2(r) =sum[r]+(r+1)ask1(r)ask2(r)

区间查询即输出 S u m [ r ] − S u m [ l − 1 ] Sum[r]-Sum[l-1] Sum[r]Sum[l1]

蓝书例题:
代码

int n,m;
ll a[maxn],c1[maxn],c2[maxn],sum[maxn],l,r,x;
char s[5];
ll ask1(ll x){ll ans=0;for(;x;x-=x&-x)ans+=c1[x];return ans;}
ll ask2(ll x){ll ans=0;for(;x;x-=x&-x)ans+=c2[x];return ans;}
void add1(ll x,ll y){for(;x<=n;x+=x&-x)c1[x]+=y;}
void add2(ll x,ll y){for(;x<=n;x+=x&-x)c2[x]+=y;}
int main()
{
    scanf("%d%d",&n,&m);
    rep(i,1,n)scanf("%lld",&a[i]),sum[i]=sum[i-1]+a[i];
    while(m--)
    {
        scanf("%s",s);
        scanf("%lld%lld",&l,&r);
        if (s[0]=='Q')
        {
            ll ans1=sum[r]-sum[l-1];
            ll ans2=(r+1)*ask1(r)-l*ask1(l-1);
            ll ans3=-ask2(r)+ask2(l-1);
            WW(ans1+ans2+ans3);
        }
        else
        {
            scanf("%lld",&x);
            add1(l,x);
            add1(r+1,-x);
            add2(l,l*x);
            add2(r+1,-(r+1)*x);
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值