0x42 树状数组

0x42 树状数组

若一个正整数 x x x的二进制表示为 a k − 1 a k − 2 . . . a 2 a 1 a 0 a_{k-1}a_{k-2}...a_2a_1a_0 ak1ak2...a2a1a0,其中等于1的位是 { a i 1 , a i 2 , . . . , a i m } \{a_{i_1},a_{i_2},...,a_{i_{m}}\} {ai1,ai2,...,aim},则正整数 x x x可以被“二进制分解”成:
x = 2 i 1 + 2 i 2 + . . . + 2 i m x=2^{i_1}+2^{i_2}+...+2^{i_m} x=2i1+2i2+...+2im
不妨设 i 1 > i 2 > . . . > i m i_1>i_2>...>i_m i1>i2>...>im,进一步地,区间 [ 1 , x ] [1,x] [1,x]可以分成 O ( l o g x ) O(logx) O(logx)个小区间:

1.长度为 2 i 1 2^{i_1} 2i1的小区间 [ 1 , 2 i 1 ] [1,2^{i_1}] [1,2i1]

2.长度为 2 i 2 2^{i_2} 2i2的小区间 [ 2 i 1 + 1 , 2 i 1 + 2 i 2 ] [2^{i_1}+1,2^{i_1}+2^{i_2}] [2i1+1,2i1+2i2]

3.长度为 2 i 3 2^{i_3} 2i3的小区间 [ 2 i 1 + 2 i 2 + 1 , 2 i 1 + 2 i 2 + 2 i 3 ] [2^{i_1}+2^{i_2}+1,2^{i_1}+2^{i_2}+2^{i_3}] [2i1+2i2+1,2i1+2i2+2i3]

……

m.长度为 2 i m 2^{i_m} 2im的小区间 [ 2 i 1 + 2 i 2 + . . . + 2 i m − 1 + 1 , 2 i 1 + 2 i 2 + . . . + 2 i m ] [2^{i_1}+2^{i_2}+...+2^{i_{m-1}}+1,2^{i_1}+2^{i_2}+...+2^{i_m}] [2i1+2i2+...+2im1+1,2i1+2i2+...+2im]

这些小区间的共同特点是:若区间结尾为 R R R,则区间长度就等于 R R R的“二进制分解”下最小的2的次幂,即 l o w b i t ( R ) lowbit(R) lowbit(R)。例如 x = 7 = 2 2 + 2 1 + 2 0 x=7=2^2+2^1+2^0 x=7=22+21+20,区间 [ 1 , 7 ] [1,7] [1,7]可以分成 [ 1 , 4 ] [1,4] [1,4] [ 5 , 6 ] [5,6] [5,6] [ 7 , 7 ] [7,7] [7,7]三个小区间,长度分别是 l o w b i t ( 4 ) = 4 lowbit(4)=4 lowbit(4)=4 l o w b i t ( 6 ) = 2 lowbit(6)=2 lowbit(6)=2 l o w b i t ( 7 ) = 1 lowbit(7)=1 lowbit(7)=1

树状数组(Binary Indexed Trees)就是一种基于上述思想的数据结构,其基本用途是维护序列的前缀和。对于给定的序列 a a a,我们建立一个数组 c c c,其中 c [ x ] c[x] c[x]保存序列 a a a的区间 [ x − l o w b i t ( x ) + 1 , x ] [x-lowbit(x)+1,x] [xlowbit(x)+1,x]中所有数的和,即 ∑ i = x − l o w b i t ( x ) + 1 x a [ i ] \sum_{i=x-lowbit(x)+1}^x a[i] i=xlowbit(x)+1xa[i]

事实上,数组 c c c可以看做一个如下图所示的树形结构,图中最下边一行是 N N N个叶子结点(N=16),代表数值 a [ 1 ∼ N ] a[1\sim N] a[1N]。该结构满足以下性质:

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)的位数。

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)

如果 N N N不是2的整次幂,那么树状数组就是一个具有同样性质的森林结构。

在这里插入图片描述

树状数组支持的基本操作有两个,第一个操作是查询前缀和,即序列 a a a 1 ∼ x 1\sim x 1x个数的和。按照我们刚才提出的方法,应该求出 x x x的二进制表示中每个等于1的位,把 [ 1 , x ] [1,x] [1,x]分成 O ( l o g N ) O(logN) O(logN)个小区间,而每个小区间的区间和都已经保存在数组 c c c中,可在 O ( l o g N ) O(logN) O(logN)的时间里查询前缀和:

int ask(int x)
{
    int ans=0;
    while(x)
    {
        ans+=c[x];
        x-=x&(-x);
	}
    return ans;
}

如果查询序列 a a a的区间 [ l , r ] [l,r] [l,r]中所有数的和,只需要计算 a s k ( r ) − a s k ( l − 1 ) ask(r)-ask(l-1) ask(r)ask(l1)

树状数组支持的第二个基本操作是单点增加,意思是给序列中的一个数 a [ x ] a[x] a[x]加上 y y y,同时正确维护序列的前缀和。只有节点 c [ x ] c[x] c[x]及其所有祖先节点保存的“区间和”包含 a [ x ] a[x] a[x],而任意一个节点的祖先至多只有 l o g N logN logN个,我们逐一对它们的 c c c值进行更新即可。下面的代码在 O ( l o g N ) O(logN) O(logN)时间内执行单点增加操作:

void add(int x,int y)
{
    while(x<=N)
    {
        c[x]+=y;
        x+=x&(-x);
	}
}

树状数组初始化,比较一般的方法是:直接建立一个全为0的数组 c c c,然后对每个位置 x x x执行 a d d ( x , a [ x ] ) add(x,a[x]) add(x,a[x]),时间复杂度为 O ( N l o g N ) O(NlogN) O(NlogN)。通常采用这种初始化方法已经足够。

更高效的方法是:从小到大依次考虑每个位置 x x x,借助 l o w b i t lowbit lowbit运算扫描他的父节点累加求和。若采用这种方法,上面树形结构中每条边只会被遍历一次,时间复杂度为 O ( ∑ k = 1 l o g N k ∗ N / 2 k ) = O ( N ) O(\sum_{k=1}^{logN}k*N/2^k)=O(N) O(k=1logNkN/2k)=O(N)

void build()
{
    for(int i=1;i<=N;++i)
    {
        c[i]+=a[i];
        int fa=i+(i&-i);
        if(fa<=N)
            c[fa]+=c[i];
	}
}

1.树状数组与逆序对

任意给定一个集合 a a a,如果用 t [ v a l ] t[val] t[val]保存数值 v a l val val在集合 a a a中出现的次数,那么数组 t t t上的区间和(即 ∑ i = 1 r t [ i ] \sum_{i=1}^rt[i] i=1rt[i]就表示集合 a a a中范围在 [ l , r ] [l,r] [l,r]内的数有多少个。

我们可以在集合 a a a的数值范围上建立一个树状数组,来维护 t t t的前缀和。这样即使在集合 a a a中插入或删除一个数,也可以高效地进行统计。

我们在0x05节中提到了逆序对问题以及使用归并排序的解法。对于一个序列 a a a,若 i < j i<j i<j a [ i ] > a [ j ] a[i]>a[j] a[i]>a[j],则称 a [ i ] a[i] a[i] a [ j ] a[j] a[j]构成逆序对。按照上述思路,利用树状数组也可以求出一个序列的逆序对个数:

1.在序列 a a a的数值范围上建立树状数组,初始化为全0。

2.倒序扫描给定的序列 a a a,对于每个数 a [ i ] a[i] a[i]

(1)在树状数组中查询前缀和 [ 1 , a [ i ] − 1 ] [1,a[i]-1] [1,a[i]1],累加到答案 a n s ans ans中。

(2)执行“单点增加”操作,即把位置 a [ i ] a[i] a[i]上的数加1(相当于 t [ a [ i ] ] t[a[i]] t[a[i]]++),同时正确维护 t t t的前缀和。这表示数值 a [ i ] a[i] a[i]又出现了1次。

3. a n s ans ans即为所求。

for(int i=n;i;--i)
{
    ans+=ask(a[i]-1);
    add(a[i],1);
}

时间复杂度为 O ( ( N + M ) l o g M ) O((N+M)logM) O((N+M)logM) M M M为数值范围的大小。

当数值范围较大时,当然可以先进行离散化,再用树状数组进行计算。不过因为离散化本身就要通过排序实现,所以在这种情况下就不如直接用归并排序来计算逆序对数了。

2.树状数组的扩展应用

区间增加

树状数组仅支持单点增加,需要做出一些转化来解决问题。

新建一个数组 b b b,起初为全零。在区间 [ l , r ] [l,r] [l,r]每个值增加 d d d,我们把它转化成以下两条指令:

1.把 b [ l ] b[l] b[l]加上 d d d

2.把 b [ r + 1 ] b[r+1] b[r+1]减去 d d d

执行完后我们考虑一下 b b b数组的前缀和( b [ 1 ∼ x ] b[1\sim x] b[1x]的和)的情况:

1.对于 1 ≤ x < l 1\leq x <l 1x<l,前缀和不变。

2.对于 l ≤ x ≤ r l\leq x \leq r lxr,前缀和增加了 d d d

3.对于 r < x ≤ N r<x\leq N r<xN,前缀和不变( l l l处加 d d d r + 1 r+1 r+1处减 d d d,抵消)。

我们发现, b b b数组的前缀和 b [ 1 ∼ x ] b[1\sim x] b[1x]就反映了指令对于 a [ x ] a[x] a[x]产生的影响。

于是我们可以用树状数组来维护数组 b b b前缀和(对 b b b只有单点增加操作)。因为各次操作之间具有可累加性,所以在树状数组上查询前缀和 b [ 1 ∼ x ] b[1\sim x] b[1x],就得出了到目前为止所有指令在 a [ x ] a[x] a[x]上增加的数值总和。再加上 a [ x ] a[x] a[x]的初始值,就得到了修改后 x x x位置上的值

在这里插入图片描述
在这里插入图片描述

查询区间的和

在区间增加的基础上我们查询区间的和。

b b b数组的前缀和 ∑ i = 1 x b [ i ] \sum_{i=1}^x b[i] i=1xb[i]就是经过这些指令后 a [ x ] a[x] a[x]增加的值。

那么序列 a a a的前缀和 a [ 1 ∼ x ] a[1\sim x] a[1x],整体增加的值就是:
∑ i = 1 x ∑ j = 1 i b [ j ] \sum_{i=1}^x \sum_{j=1}^i b[j] i=1xj=1ib[j]
上式可以改写成:
∑ i = 1 x ∑ j = 1 i b [ j ] = ∑ i = 1 x ( x − i + 1 ) ∗ b [ i ] = ( x + 1 ) ∑ i = 1 x b [ i ] − ∑ i = 1 x i ∗ b [ i ] \sum_{i=1}^x \sum_{j=1}^i b[j]=\sum_{i=1}^x (x-i+1)*b[i]=(x+1)\sum_{i=1}^x b[i]-\sum_{i=1}^x i*b[i] i=1xj=1ib[j]=i=1x(xi+1)b[i]=(x+1)i=1xb[i]i=1xib[i]
那么我们再增加一个树状数组,用于维护 i ∗ b [ i ] i*b[i] ib[i]的前缀和 ∑ i = 1 x i ∗ b [ i ] \sum_{i=1}^x i*b[i] i=1xib[i],上式就可以直接查询、计算了。

具体来说我们建立两个树状数组 c 0 c_0 c0 c 1 c_1 c1,起初全部赋值为零。对于每条指令“C l r d”,执行四个操作:

1.在树状数组 c 0 c_0 c0中,把位置 l l l上的数加 d d d

2.在树状数组 c 0 c_0 c0中,把位置 r + 1 r+1 r+1上的数减 d d d

3.在树状数组 c 1 c_1 c1中,把位置 l l l上的数加 l ∗ d l*d ld

4.在树状数组 c 1 c_1 c1中,把位置 r + 1 r+1 r+1上的数减 ( r + 1 ) ∗ d (r+1)*d (r+1)d

另外,我们建立数组 s u m sum sum存储序列 a a a的原始前缀和。对于每天指令“Q l r”,当然还是拆分成 1 ∼ r 1\sim r 1r 1 ∼ l − 1 1\sim l-1 1l1两个部分,二者相减。写成式子就是:
( s u m [ r ] + ( r + 1 ) ∗ a s k ( c 0 , r ) − a s k ( c 1 , r ) ) − ( s u m [ l − 1 ] + l ∗ a s k ( c 0 , l − 1 ) − a s k ( c 1 , l − 1 ) ) (sum[r]+(r+1)*ask(c_0,r)-ask(c_1,r))-(sum[l-1]+l*ask(c_0,l-1)-ask(c_1,l-1)) (sum[r]+(r+1)ask(c0,r)ask(c1,r))(sum[l1]+lask(c0,l1)ask(c1,l1))

typedef long long ll;
int N,Q,a,b,c;
char op;
ll arr[100005];
ll sum[100005];
ll tr1[100005];
ll tr2[100005];

ll ask(ll tr[],int x)
{
    ll ans=0;
    while(x)
    {
        ans+=tr[x];
        x-=x&(-x);
    }
    return ans;
}

void add(ll tr[],int x,ll y)
{
    while(x<=N)
    {
        tr[x]+=y;
        x+=x&(-x);
    }
}

int main()
{
    scanf("%d%d",&N,&Q);
    for(int i=1;i<=N;++i)
    {
        scanf("%lld",&arr[i]);
        sum[i]=sum[i-1]+arr[i];
    }
    while(Q--)
    {
        cin>>op;
        if(op=='Q')
        {
            scanf("%d%d",&a,&b);
            ll ans=sum[b]-sum[a-1];
            ans+=(b+1)*ask(tr1,b)-ask(tr2,b)-a*ask(tr1,a-1)+ask(tr2,a-1);
            printf("%lld\n",ans);
        }
        else
        {
            scanf("%d%d%d",&a,&b,&c);
            add(tr1,a,c);
            add(tr1,b+1,-c);
            add(tr2,a,a*c);
            add(tr2,b+1,-(b+1)*c);
        }
    }
    return 0;
}

值得指出的是,为什么我们把 ∑ i = 1 x ( x − i + 1 ) ∗ b [ i ] \sum_{i=1}^x (x-i+1)*b[i] i=1x(xi+1)b[i]变成 ( x + 1 ) ∑ i = 1 x b [ i ] − ∑ i = 1 x i ∗ b [ i ] (x+1)\sum_{i=1}^x b[i]-\sum_{i=1}^x i*b[i] (x+1)i=1xb[i]i=1xib[i]进行统计?这里的 x x x是关于“前缀和 a [ 1 ∼ x ] a[1\sim x] a[1x]”这个询问的变量,而 i i i是在每次修改时影响的对象。

对于前者来说,求和式中每一项同时包含 x x x i i i,在修改时无法确定 ( x − i + 1 ) (x-i+1) (xi+1)的值,只能维护 b [ i ] b[i] b[i]的前缀和。在询问时需要面临一个“系数为等差数列”的求和式,计算起来非常困难。

对于后者来说,求和式中的每一项只与 i i i有关。它通过一次容斥,把 ( x + 1 ) (x+1) (x+1)提取为常量,使得 b [ i ] b[i] b[i]的前缀和与 i ∗ b [ i ] i*b[i] ib[i]的前缀和可以分别用树状数组进行维护。这种分离包含有多个变量的项,使公式中不同变量之间相互独立的思想非常重要,我们在下一章讨论动态规划时会多次用到。

  • 28
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

谷神星ceres

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值