树状数组基础
树状数组是一个查询和修改复杂度都为log(n)的数据结构。主要用于数组的单点修改&&区间求和. 另外一个拥有类似功能的是线段树. 具体区别和联系如下: 1.两者在复杂度上同级, 但是树状数组的常数明显优于线段树, 其编程复杂度也远小于线段树. 2.树状数组的作用被线段树完全涵盖, 凡是可以使用树状数组解决的问题, 使用线段树一定可以解决, 但是线段树能够解决的问题树状数组未必能够解决. 3.树状数组的突出特点是其编程的极端简洁性, 使用lowbit技术可以在很短的几步操作中完成树状数组的核心操作,其代码效率远高于线段树。 上面出现了一个新名词:lowbit.其实lowbit(x)就是求x最低位的1;
下面加图进行解释 对于一般的二叉树,我们是这样画的 把位置稍微移动一下,便是树状数组的画法
上图其实是求和之后的数组,原数组和求和数组的对照关系如下,其中a数组是原数组,c数组是求和后的数组:
C[i]代表 子树的叶子结点的权值之和 如图可以知道
C[1]=A[1];
C[2]=A[1]+A[2];
C[3]=A[3];
C[4]=A[1]+A[2]+A[3]+A[4];
C[5]=A[5];
C[6]=A[5]+A[6];
C[7]=A[7];
C[8]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];
再将其转化为二进制看一下:
C[1] = C[0001] = A[1];
C[2] = C[0010] = A[1]+A[2];
C[3] = C[0011] = A[3];
C[4] = C[0100] = A[1]+A[2]+A[3]+A[4];
C[5] = C[0101] = A[5];
C[6] = C[0110] = A[5]+A[6];
C[7] = C[0111] = A[7];
C[8] = C[1000] = A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];
对照式子可以发现 $C[i]=A[i-2^k+1]+A[i-2^k+2]+……A[i]$;
($k$ 为 $i$ 的二进制中从最低位到高位连续零的长度)例如 $i=8(1000)$ 时,$k=3$;
$C[8] = A[8-2^3+1]+A[8-2^3+2]+……+A[8]$ 即为上面列出的式子 现在我们返回到
lowbit
中来 其实不难看出 lowbit(i)
便是上面的 $2^k$ 因为 $2^k$ 后面一定有 $k$ 个 $0$ 比如说$2^5==>100000$ 正好是 $i$ 最低位的 1 加上后缀 0 所得的值 开篇就说了,lowbit(x)
是取出 x 的最低位 1;具体操作为
int lowbit(x){return x&(-x);}
极致简短! 现在我们来理解一下这行代码: 我们知道,对于一个数的负数就等于对这个数取反 +1 以二进制数 11010 为例: 11010 的补码为 00101,加 1 后为 00110,两者相与便是最低位的 1 其实很好理解,补码和原码必然相反,所以原码有 0 的部位补码全是 1 ,补码再 +1 之后由于进位那么最末尾的 1 和原码 最右边的 1 一定是同一个位置(当遇到第一个 1 的时候补码此位为 0 ,由于前面会进一位,所以此位会变为 1 ) 所以我们只需要进行a&(-a)
就可以取出最低位的 1 了 会了lowbit,我们就可以进行区间查询和单点更新了.
单点更新:
继续看开始给出的图 此时如果我们要更改A[1] 则有以下需要进行同步更新
1(001)
C[1]+=A[1]
lowbit(1)=001 1+lowbit(1)=2(010) C[2]+=A[1]
lowbit(2)=010 2+lowbit(2)=4(100) C[4]+=A[1]
lowbit(4)=100 4+lowbit(4)=8(1000) C[8]+=A[1]
换成代码就是:
1 | void update(int x,int y,int n){ |
区间查询:
举个例子
i=5
C[4]=A[1]+A[2]+A[3]+A[4];
C[5]=A[5];
可以推出: sum(i = 5) ==> C[4]+C[5];
序号写为二进制: sum(101)=C[(100)]+C[(101)];
第一次 101,减去最低位的 1 就是 100;
其实也就是单点更新的逆操作 代码如下
1 |
|
lowbit 会了,区间查询有了,单点更新也有了接下来该做题了 单击传送门移步HDU1166敌兵布阵
附代码:
1 |
|
高级操作
求逆序对
操作
对于数组 a,我们将其离散化处理为数组 b.区间查询与单点修改代码如下
1 |
|
a 的逆序对个数为:
1 | for(int i=1;i<=n;i++){ |
res 就是逆序对个数,ask,需注意b[i]
应该大于0
原理
此部分来自 ssimple_y 的博客 第一次插入的时候把5这个位置上加上1,read(x)值就是1,当前已经插入了一个数,所以他前面比他大的数的个数就等于 i - read(x) = 1 - 1 = 0,所以总数 sum += 0 第二次插入的时候,read(x)的值同样是1,但是 i - read(x) = 2 - 1 = 1,所以sum += 1
第三次的时候,read(x)的值是2,i - read(x) = 3 - 2 = 1,所以sum += 1
第四次,read(x)的值是1,i - read(x) = 4 - 1 = 3,所以sum += 3
第五次,read(x)的值是1,i - read(x) = 5 - 1 = 4,所以sum += 4
这样整个过程就结束了,所有的逆序对就求出来了。
求区间最大值
1 | void Update(int i,int v) |
该部分内容转自 胡小兔的OI博
区间修改+单点查询
通过“差分”(就是记录数组中每个元素与前一个元素的差),可以把这个问题转化为问题1。
查询
设原数组为a[i]
, 设数组d[i]=a[i]-a[i-1](a[0]=0)
,则$a[i]=\sum_{j=1}^{i}d[j]$,可以通过d[i]
求的前缀和查询。
修改
当给区间[l,r]
加上x的时候,a[l]
与前一个元素 a[l-1]
的差增加了x,a[r+1]
与 a[r]
的差减少了x。根据d[i]
数组的定义,只需给a[l]加上 x, 给a[r+1] 减去x即可
1 | void add(int p, int x){ //这个函数用来在树状数组中直接修改 |
区间修改+区间查询
这是最常用的部分,也是用线段树写着最麻烦的部分——但是现在我们有了树状数组! 怎么求呢?我们基于问题2的“差分”思路,考虑一下如何在问题2构建的树状数组中求前缀和: 位置p的前缀和 = 在等式最右侧的式子$sum{i=1}^{p}sum{j=1}^{i}d[j]$中,d[1]被用了p次,d[2]被用了p-1次……那么我们可以写出: 位置p的前缀和 =
那么我们可以维护两个数组的前缀和:
一个数组是 $sum1[i]=d[i]$
另一个数组是 $sum2[i]=d[i]*i$
查询
位置p的前缀和即:$(p+1)*sum1$数组中p的前缀和 - sum2数组中p的前缀和。 区间[l, r]的和即:位置r的前缀和 - 位置l的前缀和。
修改
对于sum1数组的修改同问题2中对d数组的修改。 对于sum2数组的修改也类似,我们给 sum2[l] 加上 l x,给 sum2[r + 1] 减去 (r + 1) x。
1 | void add(ll p, ll x){ |
用这个做区间修改区间求和的题,无论是时间上还是空间上都比带lazy标记的线段树要优。 二维树状数组 我们已经学会了对于序列的常用操作,那么我们不由得想到(谁会想到啊喂)……能不能把类似的操作应用到矩阵上呢?这时候我们就要写二维树状数组了! 在一维树状数组中,tree[x](树状数组中的那个“数组”)记录的是右端点为x、长度为lowbit(x)的区间的区间和。 那么在二维树状数组中,可以类似地定义tree[x][y]
记录的是右下角为(x, y),高为lowbit(x)
, 宽为 lowbit(y)
的区间的区间和。
单点修改+区间查询
1 | void add(int x, int y, int z){ //将点(x, y)加上z |
区间修改 + 单点查询
我们对于一维数组进行差分,是为了使差分数组前缀和等于原数组对应位置的元素。 那么如何对二维数组进行差分呢?可以针对二维前缀和的求法来设计方案。 二维前缀和: $sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+a[i][j]$ 那么我们可以令差分数组$d[i][j]$表示$a[i][j]$与$a[i-1][j]+a[i][j-1]-a[i-1][j-1]$的差。 例如下面这个矩阵
1 4 8
6 7 2
3 9 5
对应的差分数组就是
1 3 4
5 -2 -9
-3 5 1
当我们想要将一个矩阵加上x时,怎么做呢? 下面是给最中间的3*3矩阵加上x时,差分数组的变化:
0 0 0 0 0
0 +x 0 0 -x
0 0 0 0 0
0 0 0 0 0
0 -x 0 0 +x
这样给修改差分,造成的效果就是:
0 0 0 0 0
0 x x x 0
0 x x x 0
0 x x x 0
0 0 0 0 0
那么我们开始写代码吧!
1 | void add(int x, int y, int z){ |
区间修改 + 区间查询
类比之前一维数组的区间修改区间查询,下面这个式子表示的是点(x, y)的二维前缀和: $\sum{i=1}^{x}\sum{j=1}^{y}\sum{k=1}^{i}\sum{h=1}^{j}d[h][k]$
(d[h][k]为点(h, k)对应的“二维差分”(同上题))
这个式子炒鸡复杂:$O(n^4)$的时间复杂度!,但利用树状数组,我们可以把它优化到$O(log2n)$! 首先,类比一维数组,统计一下每个$d[h][k]$出现过多少次。$d[1][1]$出现了$x_y$次,$d[1][2]$出现了$x(y-1)$次……$d[h][k]$出现了$(x-h+1)(y-k+1)$次。 那么这个式子就可以写成: $sum{i=1}^{x}sum{j=1}^{y}d[i][j](x+1-i)(y+1-j)$ 把这个式子展开,就得到: 那么我们要开四个树状数组,分别维护: $d[i][j]_j$,$d[i][j]_i$,$d[i][j]_j$,$d[i][j]_ij$ 这样就完成了!
1 |
|