全网最强剖析树状数组 | 万字长文爆肝树状数组

树状数组

预备知识


lowbit函数

l o w b i t ( x ) = x lowbit(x)=x lowbit(x)=x& ( − x ) (-x) (x)。这个式子的含义: x x x的二进制表示下最右边的1和它右边所有的0。因此,它一定是2的幂次方,即1、2、4、8等。

例如,设 x = ( 0000001101001100 ) 2 x=(0000001101001100)_2 x=(0000001101001100)2,由补码知识可知, − x = ( 1111110010110100 ) 2 -x=(1111110010110100)_2 x=(1111110010110100)2。所以, x x x& ( − x ) = ( 0000000000000100 ) 2 = 4 (-x)=(0000000000000100)_2=4 (x)=(0000000000000100)2=4

再比如,设 x = 6 = ( 110 ) 2 x=6=(110)_2 x=6=(110)2,由补码知识可知, − x = ( 010 ) 2 -x=(010)_2 x=(010)2。所以, x x x& ( − x ) = ( 010 ) 2 = 2 (-x)=(010)_2=2 (x)=(010)2=2

lowbit(x)也可以理解为能整除x的最大的2的幂次。例如x=12,那么lowbit(12)=4,而 4 = 2 2 4=2^2 4=22,此时指数2已经是最大的幂次了。因此,4也就是能整数12的最大2的幂次。

[


二进制知识

根据任意正整数关于2的不重复次幂的唯一分解性质,若一个正整数x的二进制表示为 a k − 1 a k − 2 ⋯ a 2 a 1 a 0 a_{k-1}a_{k-2}\cdots a_2a_1a_0 ak1ak2a2a1a0,其中等于1的位是{ a i 1 , a i 2 , ⋯   , a i m a_{i_1},a_{i_2},\cdots ,a_{i_m} ai1,ai2,,aim},则正整数x可以被"二进制分解"成:

x = 2 i 1 + 2 i 2 + ⋯ + 2 i m x=2^{i_1}+2^{i_2}+\cdots +2^{i_m} x=2i1+2i2++2im。这里计算的都是二进制位是1的,因为二进制位是0的对结果 x x x并没有任何贡献。

不妨设 i 1 > i 2 > ⋯ i m i_1>i_2>\cdots i_m i1>i2>im,进一步地,区间 [ 1 , x ] [1,x] [1,x]可以分成 O ( l o g x ) O(logx) O(logx)个小的区间:

  • 长度为 2 i 1 2^{i_1} 2i1的小区间 [ 1 , 2 i 1 ] [1,2^{i_1}] [1,2i1]
  • 长度为 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]
  • 长度为 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]
  • ⋯ \cdots
  • 长度为 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}+\cdots +2^{i_m-1}+1,2^{i_1}+2^{i_2}+\cdots +2^{i_m}] [2i1+2i2++2im1+1,2i1+2i2++2im]

这些小区间的共同特点是:若区间结尾为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]三个小区间,对于第一个小区间 [ 1 , 4 ] [1,4] [1,4]来说, R = 4 = ( 100 ) 2 R=4=(100)_2 R=4=(100)2,其"二进制分解"下最小的2的幂次是 2 2 = 4 2^2=4 22=4,因此区间长度为 l o w b i t ( 4 ) = 4 lowbit(4)=4 lowbit(4)=4;对于第二个小区间 [ 5 , 6 ] [5,6] [5,6]来说, R = 6 = ( 110 ) 2 R=6=(110)_2 R=6=(110)2,其"二进制分解"下最小的2的幂次是 2 1 = 2 2^1=2 21=2,因此区间长度为 l o w b i t ( 6 ) = 2 lowbit(6)=2 lowbit(6)=2;对于第三个小区间 [ 7 , 7 ] [7,7] [7,7]来说, R = 7 = ( 111 ) 2 R=7=(111)_2 R=7=(111)2,其"二进制分解"下最小的2的幂次是 2 0 = 1 2^0=1 20=1,因此区间长度为 l o w b i t ( 7 ) = 1 lowbit(7)=1 lowbit(7)=1

下面这段代码可以计算出区间 [ 1 , x ] [1,x] [1,x]分成的 0 ( l o g x ) 0(logx) 0(logx)个小区间:

while(x>0)
{
    int t=x&(-x);		//相当于计算lowbit(x),根据区间长度=lowbit(R)可知,计算出的t是区间长度
    int L=x-t+1;		//区间的右端点是x,那么左端点L=x-t+1
    printf("[%d,%d]\n",L,x);	//输出这个小区间
    x-=t;	//获得下一个小区间的右端点
}

树状数组及其应用

树状数组 ( B i n a r y I n d e x e d T r e e , B I T ) (Binary\quad Indexed\quad Tree,BIT) (BinaryIndexedTreeBIT)。它其实仍然是一个数组,是用来记录和的数组,只不过它存放的不是前i个整数的和,而是在i号位之前(包括i号位)lowbit(i)个整数之和,即以i为右端点,长度为 l o w b i t ( i ) lowbit(i) lowbit(i)的区间中所有数的和。即 C [ i ] C[i] C[i]存放的是区间 [ i − l o w b i t ( i ) + 1 , i ] [i-lowbit(i)+1,i] [ilowbit(i)+1,i]中所有整数的和。如图13-2所示,数组A是原始数组,有 A [ 1 ]   A [ 16 ] A[1]~A[16] A[1] A[16]共16个元素;数组C是树状数组,其中 C [ i ] C[i] C[i]存放数组A中i号位(含i)之前 l o w b i t ( i ) lowbit(i) lowbit(i)个元素之和。 C [ i ] C[i] C[i]的覆盖长度是 l o w b i t ( i ) lowbit(i) lowbit(i)(覆盖长度,也就是区间长度,也可以理解成管辖范围),它一定是2的幂次,即1、2、4、8。

对于给定的序列A,我们建立一个树状数组C,其中C[x]保存序列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 \limits _{i=x-lowbit(x)+1}^{x}A[i] i=xlowbit(x)+1xA[i]

树状数组C可以看作是如下图所示的树形结构,图中最下边一行是N个叶子节点(N=16),代表数值A[1]~A[N]。该结构满足以下性质:

  • 每个内部节点C[x]保存以它为根的子树中的所有叶子节点的和。
  • 每个内部节点C[x]的孩子节点个数等于 l o w b i t ( x ) lowbit(x) lowbit(x)的位数。
  • 除了树根之外,每个内部节点C[x]的父节点是C[x+lowbit(x)]。
  • 树的深度为 O ( l o g N ) O(logN) O(logN)

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  • C[1]=A[1] 长度为 l o w b i t ( 1 ) = 1 lowbit(1)=1 lowbit(1)=1
  • C[2]=A[1]+A[2] 长度为 l o w b i t ( 2 ) = 2 lowbit(2)=2 lowbit(2)=2
  • C[3]=A[3] 长度为 l o w b i t ( 3 ) = 1 lowbit(3)=1 lowbit(3)=1
  • C[4]=A[1]+A[2]+A[3]+A[4] 长度为 l o w b i t ( 4 ) = 4 lowbit(4)=4 lowbit(4)=4
  • C[5]=A[5] 长度为 l o w b i t ( 5 ) = 1 lowbit(5)=1 lowbit(5)=1
  • C[6]=A[5]+A[6] 长度为 l o w b i t ( 6 ) = 2 lowbit(6)=2 lowbit(6)=2
  • C[7]=A[7] 长度为 l o w b i t ( 7 ) = 1 lowbit(7)=1 lowbit(7)=1
  • C[8]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8] 长度为 l o w b i t ( 8 ) = 8 lowbit(8)=8 lowbit(8)=8
  • C[9]=A[9] 长度为 l o w b i t ( 9 ) = 1 lowbit(9)=1 lowbit(9)=1
  • C[10]=A[9]+A[10] 长度为 l o w b i t ( 10 ) = 2 lowbit(10)=2 lowbit(10)=2
  • C[11]=A[11] 长度为 l o w b i t ( 11 ) = 1 lowbit(11)=1 lowbit(11)=1
  • C[12]=A[9]+A[10]+A[11]+A[12] 长度为 l o w b i t ( 12 ) = 4 lowbit(12)=4 lowbit(12)=4
  • C[13]=A[13] 长度为 l o w b i t ( 13 ) = 1 lowbit(13)=1 lowbit(13)=1
  • C[14]=A[13]+A[14] 长度为 l o w b i t ( 14 ) = 2 lowbit(14)=2 lowbit(14)=2
  • C[15]=A[15] 长度为 l o w b i t ( 15 ) = 1 lowbit(15)=1 lowbit(15)=1
  • C[16]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8]+A[9]+A[10]+A[11]+A[12]+A[13]+A[14]+A[15]+A[16] 长度为 l o w b i t ( 16 ) = 16 lowbit(16)=16 lowbit(16)=16

树状数组有两点必须注意:

  • C[i]的覆盖长度是 l o w b i t ( i ) lowbit(i) lowbit(i)
  • 树状数组的起始下标必须从1开始,而不是从0开始

查询

问题:设计一个函数getSum(int x),返回前x个数之和A[1]+A[2]+…+A[X]

假设要查询A[1]+A[2]+…+A[14],由图13-2可知,A[1]+A[2]+…+A[14]=C[8]+C[12]+C[14],再比如要查询A[1]+A[2]+…+A[11],由图13-2可知,A[1]+A[2]+…+A[11]=C[8]+C[10]+C[11]。那么我们怎么知道,A[1]+…+A[x]对应的是树状数组中的哪些项呢?

记SUM(1,x)=A[1]+…+A[x],因为C[x]的覆盖长度是 l o w b i t ( x ) lowbit(x) lowbit(x),因此, C [ x ] = A [ x − l o w b i t ( x ) + 1 ] + . . . + A [ x ] C[x]=A[x-lowbit(x)+1]+...+A[x] C[x]=A[xlowbit(x)+1]+...+A[x]。于是可以得到:

SUM(1,x)=A[1]+…+A[x]=(A[1]+…+A[ x − l o w b i t ( x ) x-lowbit(x) xlowbit(x)])+A[ x − l o w b i t ( x ) + 1 x-lowbit(x)+1 xlowbit(x)+1]+…+A[x]=SUM(1, x − l o w b i t ( x ) x-lowbit(x) xlowbit(x))+C[x]

这样我们就把SUM(1,x)转换为SUM(1, x − l o w b i t ( x ) x-lowbit(x) xlowbit(x))了。

在这里插入图片描述

在这里插入图片描述

于是,就可以很容易写出getSum(int x)函数了

//返回前x个整数之和
int getSum(int x)
{
    int sum=0;	//记录和
    for(int i=x;i>0;i-=lowbit(i))	//注意是i>0而不是i>=0,因为树状数组下标从1开始,不可能取到0
    {
        sum+=c[i];	//累计C[i],然后把问题规模缩小为SUM(1,i-lowbit(i))
    }
    return sum;	//返回和
}

显然,由于 l o w b i t ( i ) lowbit(i) lowbit(i)的作用是定位 i i i的二进制中最右边的1,因此 i = i − l o w b i t ( i ) i=i-lowbit(i) i=ilowbit(i)事实上就是不断把 i i i的二进制中最右边的1置为0的过程。所以getSum函数的for循环执行次数为x的二进制中1的个数,也就是说,getSum函数的时间复杂度是 O ( l o g N ) O(logN) O(logN)。getSum函数的过程实际上是在沿着一条不断左上的路径行进(可以想想getSum(11)和getSum(14)的过程)。

如果要求区间和,比如求区间 [ x , y ] [x,y] [x,y]内的整数之和,即 A [ x ] + A [ x + 1 ] + ⋯ + A [ y ] A[x]+A[x+1]+\cdots +A[y] A[x]+A[x+1]++A[y],可以转换成getSum(y)-getSum(x-1)来解决,这就类似于前缀和想要求解区间 [ l , r ] [l,r] [l,r]中的整数之和为 S [ r ] − S [ l − 1 ] S[r]-S[l-1] S[r]S[l1]

在这里插入图片描述

在这里插入图片描述


更新

问题 :设计函数update(int x,int v),实现将第x个数加上一个数v的功能。

假设要让A[6]加上一个数v,那么就要寻找树状数组C中能覆盖A[6]的元素,让它们都加上v。由图13-2可知,覆盖A[6]的有C[6]、C[8]、C[16],那么就让它们都加上v即可;再如,要让A[9]加上一个数v,实际上就是要让C[9]、C[10]、C[12]、C[16]都将加上v即可。想要给A[x]加上v时,怎样去寻找树状数组中的对应项呢?

要让A[x]加上v,就是要寻找树状数组C中覆盖A[x]的那些元素,让它们都加上v。由图13-2可知,只需要总是寻找离当前"矩形"C[x]最近的"矩形"C[y],使得C[y]能够覆盖C[x]即可。例如要让A[5]加上v,就要从C[5]开始找起:离C[5]最近的能覆盖C[5]的"矩形"是C[6],离C[6]最近的能覆盖C[6]的"矩形"是C[8],离C[8]最近的能覆盖C[8]的"矩形"是C[16],因此,只需要把C[5]、C[6]、C[8]、C[16]都加上v即可。那么,如何找到距离当前C[x]最近的能覆盖C[x]的C[y]呢?

首先,在前面我们已经提过了,C[i]的覆盖长度就是 l o w b i t ( i ) lowbit(i) lowbit(i)。要想让C[y]覆盖C[x],显然,必须让 l o w b i t ( y ) > l o w b i t ( x ) lowbit(y)>lowbit(x) lowbit(y)>lowbit(x)(不然怎么能覆盖呢?)。因为是要找最近的,因此问题就转换为求一个尽可能小的整数a,使得 l o w b i t ( x + a ) > l o w b i t ( x ) lowbit(x+a)>lowbit(x) lowbit(x+a)>lowbit(x)。由于 l o w b i t ( x ) lowbit(x) lowbit(x)是取x的二进制最右边的1的位置,因此如果 l o w b i t ( a ) < l o w b i t ( x ) lowbit(a)<lowbit(x) lowbit(a)<lowbit(x),那么则一定有 l o w b i t ( x + a ) < l o w b i t ( x ) lowbit(x+a)<lowbit(x) lowbit(x+a)<lowbit(x),比如 a = 2 = ( 010 ) 2 a=2=(010)_2 a=2=(010)2 x = 4 = ( 100 ) 2 x=4=(100)_2 x=4=(100)2 a + x = 2 + 4 = 6 = ( 110 ) 2 a+x=2+4=6=(110)_2 a+x=2+4=6=(110)2 l o w b i t ( x + a ) = l o w b i t ( 6 ) = 2 lowbit(x+a)=lowbit(6)=2 lowbit(x+a)=lowbit(6)=2 l o w b i t ( x ) = 4 lowbit(x)=4 lowbit(x)=4,因此, l o w b i t ( x + a ) = 2 < l o w b i t ( x ) = 4 lowbit(x+a)=2<lowbit(x)=4 lowbit(x+a)=2<lowbit(x)=4。由此可知, l o w b i t ( a ) lowbit(a) lowbit(a)必须不能小于 l o w b i t ( x ) lowbit(x) lowbit(x)。接着发现,当a取 l o w b i t ( x ) lowbit(x) lowbit(x)时,由于x和a的二进制最右边的1的位置相同,因此x+a会在这个1的位置产生进位,使得进位过程中所有连续二进制位为1的都变成0,直到把它们左边第一个0置为1时结束。举个栗子,比如x=11011100,a=100,那么x+a=11100000,此时 l o w b i t ( x + a ) = 32 > l o w b i t ( x ) = 4 lowbit(x+a)=32>lowbit(x)=4 lowbit(x+a)=32>lowbit(x)=4,于是 l o w b i t ( x + a ) > l o w b i t ( x ) lowbit(x+a)>lowbit(x) lowbit(x+a)>lowbit(x)显然成立,最小的a就是lowbit(x)

因此,update函数的方法就是:只要让x不断加上 l o w b i t ( x ) lowbit(x) lowbit(x),并让每一步的C[x]都加上v,直到x超过给的数据范围为止(因为在不给定范围的情况下,更新操作是无上限的)。

//将第x个整数加上v
void update(int x,int v)
{
    //i是从第x个整数开始,然后原始数组A最大是到第N个整数,所以i可以取到第N个整数更新。
    for(int i=x;i<=N;i+=lowbit(i))
    {
        c[i]+=v;	//让c[i]加上v,然后找到其父节点更新,让c[i+lowbit(i)]加上v
    }
}

显然,这个过程是从右往左不断定位x的二进制最右边的1左边0的过程。因此update函数的时间复杂度是 O ( l o g N ) O(log N) O(logN)。update函数的过程实际上是在沿着一条不断右上的路径行进。

在这里插入图片描述

在这里插入图片描述

查询过程每次就是减去了二进制中的低位1,即 i = i − l o w b i t ( i ) i=i-lowbit(i) i=ilowbit(i)。(1111 - 1 -> 1110, 1110 - 10 -> 1100, 1100 - 100 -> 1000)

更新过程每次就是加上了二进制的低位1,即 i = i + l o w b i t ( i ) i=i+lowbit(i) i=i+lowbit(i)。(101+1 ->110, 110 + 10 -> 1000, 1000 + 1000 -> 10000)

在这里插入图片描述


初始化

在执行所有操作之前,我们需要先对树状数组进行初始化—针对原始序列A构造一个树状数组。为了方便,比较一般的初始化构造方式是:直接建立一个全0的数组C,然后对每个位置x执行更新操作update(int x,int v),就完成了对原始序列A构造树状数组的过程,时间复杂度是 O ( N l o g N ) O(NlogN) O(NlogN)


树状数组的应用

题目描述

给定一个有N个正整数的序列A( N ≤ 1 0 5 , A [ i ] ≤ 1 0 5 N\leq 10^5,A[i]\leq 10^5 N105,A[i]105),对序列中的每个数,求出序列中它左边比它小的数的个数。

比如序列{2,5,1,3,4},A[1]=2,在A[1]左边比A[1]小的数的个数有0个;A[2]=5,在A[2]左边比A[2]小的数的个数有1个,即2;A[3]=1,在A[3]左边比A[3]小的数的个数有0个;A[4]=3,在A[4]左边比A[4]小的数的个数有2个,即2,1;A[5]=4,在A[5]左边比A[5]小的数有3个,即2,1,3。

先来看使用hash数组的做法,hash[x]记录整数x当前出现的次数。从左往右遍历序列A,假设当前访问的是A[i],那么就令hash[A[i]]加1,即hash[A[i]]++,表示当前整数A[i]的出现次数增加了一次;同时,序列中在A[i]左边比A[i]小的数的个数为:hash[1]+hash[2]+…+hash[A[i]-1]。其中 1 < 2 < 3 < ⋯ < A [ i ] − 1 < A [ i ] 1<2<3<\cdots <A[i]-1<A[i] 1<2<3<<A[i]1<A[i]。hash[1]表示元素1出现的次数,hash[2]表示元素2出现的次数,… ,hash[A[i]-1]表示元素A[i]-1出现的次数,因此,比A[i]小的数的个数总和为:hash[1]+hash[2]+…+hash[A[i]-1]。

但是我们可以发现,hash[A[i]]++,这个操作可以用树状数组中的update(A[i],1)来代替;而hash[1]+hash[2]+…+hash[A[i]-1]就相当于求区间[1,A[i]-1]中的前缀和,而这个操作可以用树状数组中的getSum(A[i]-1)来代替。

使用树状数组时,不必真的开一个hash数组,因为它只是存在于解法的逻辑中,并不需要真正的用到,只是帮助我们理解为什么这道题可以由hash数组联想到转换为树状数组来解题。我们只需要一个树状数组来代替hash数组即可。

代码

#include<iostream>
#include<cstring>
using namespace std;
const int N=100010;
int n;
int c[N];
int lowbit(int x)
{
    return x&(-x);
}
//将第x个整数加上v
void update(int x,int v)
{
    for(int i=x;i<=n;i+=lowbit(i))
    {
        c[i]+=v;
    }
}
//返回前x个整数之和
int getSum(int x)
{
    int sum=0;	//记录和
    for(int i=x;i>0;i-=lowbit(i))
    {
        sum+=c[i];
    }
    return sum;
}
int main()
{
    int x;
    scanf("%d",&n);
    memset(c,0,sizeof c);	//树状数组初始值为0
    for(int i=0;i<n;i++)	//输入n个数
    {	
        scanf("%d",&x);	//输入序列元素
        update(x,1);	//x的出现次数+1
        int cnt=getSum(x);	//查询当前小于x的数的个数
        printf("%d\n",cnt);
    }
    return 0;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

卷心菜不卷Iris

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

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

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

打赏作者

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

抵扣说明:

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

余额充值