树状数组
预备知识
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 ak−1ak−2⋯a2a1a0,其中等于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+⋯+2im−1+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) (BinaryIndexedTree,BIT)。它其实仍然是一个数组,是用来记录和的数组,只不过它存放的不是前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] [i−lowbit(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] [x−lowbit(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=x−lowbit(x)+1∑xA[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[x−lowbit(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) x−lowbit(x)])+A[ x − l o w b i t ( x ) + 1 x-lowbit(x)+1 x−lowbit(x)+1]+…+A[x]=SUM(1, x − l o w b i t ( x ) x-lowbit(x) x−lowbit(x))+C[x]
这样我们就把SUM(1,x)转换为SUM(1, x − l o w b i t ( x ) x-lowbit(x) x−lowbit(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=i−lowbit(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[l−1]。
更新
问题 :设计函数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=i−lowbit(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 N≤105,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;
}