树状数组(Markdown版本)

什么是树状数组:

树状数组(Binary Indexed Tree(B.I.T), Fenwick Tree)是一个查询和修改复杂度都为 l o g ( n ) log(n) log(n)的数据结构。

它能用来干什么:

最经典的应用就是查询某一段区间元素的和,而且支持在线修改操作。如果没有修改操作,就没必要用树状数组了。
在这里插入图片描述

它和线段树有什么关系:

实际上树状数组就是没有右儿子的线段树,它实现起来比线段树简单,而且效率更高,空间占用的也更少。但是能用树状数组解决的问题,基本上都能用线段树解决。(反过来就不是了)
我还是不知道树状数组是什么:
不多解释了,先上几张图:
在这里插入图片描述

我们用数组A表示原序列: A 1 、 A 2 … … A n A1、A2……An A1A2An
用数组 C C C代表树状数组,从上图中我们可以发现:
C 1 = A 1 C1=A1 C1=A1
C 2 = A 1 + A 2 C2=A1+A2 C2=A1+A2
C 3 = A 3 C3=A3 C3=A3
C 4 = A 1 + A 2 + A 3 + A 4 C4=A1+A2+A3+A4 C4=A1+A2+A3+A4
C 5 = A 5 C5=A5 C5=A5
C 6 = A 5 + A 6 C6=A5+A6 C6=A5+A6
C 7 = A 7 C7=A7 C7=A7
C 8 = A 1 + A 2 + A 3 + A 4 + A 5 + A 6 + A 7 + A 8 C8=A1+A2+A3+A4+A5+A6+A7+A8 C8=A1+A2+A3+A4+A5+A6+A7+A8
考虑数组 C C C下标的二进制表示与求和元素个数的关系:
C 1 C1 C1的下标1的二进制表示 1 末尾有0个连续的0 该节点管理1个元素
C 2 C2 C2的下标2的二进制表示 10 末尾有1个连续的0 该节点管理2个元素
C 3 C3 C3的下标3的二进制表示 11 末尾有0个连续的0 该节点管理1个元素
C 4 C4 C4的下标4的二进制表示 100 末尾有2个连续的0 该节点管理4个元素
……
由此我们得到一个规律:我们设 i i i的二进制表示的末尾有 k k k个连续的0,那么 C i Ci Ci管理 2 k 2^k 2k个元素,即等于 2 k 2^k 2k个元素之和(这里的和只是运算符的一种形式,事实上问题并不一定是求和,所以用管理更为恰当)。那么有什么办法可以很快的算出来 2 k 2^k 2k呢?我们有一个名为 l o w b i t lowbit lowbit的神奇操作: l o w b i t ( x ) = x & ( − x ) lowbit(x)=x\&(-x) lowbit(x)=x&(x) 。它就是我们想要的答案。且对于节点 i i i i + l o w b i t ( i ) i+lowbit(i) i+lowbit(i)就是节点 i i i的父节点, i − l o w b i t ( i ) i-lowbit(i) ilowbit(i)就是节点 i i i的左兄弟节点。

知道这些有什么用呢?

我们可以对照上文中的图,若求 [ 1 , 7 ] [1,7] [1,7]的区间和,那么 s u m = C 7 + C 6 + C 4 sum=C_7+C_6+C_4 sum=C7+C6+C4,三次操作就得到了我们想要的结果,再看看 l o w b i t lowbit lowbit发挥了怎样的作用吧: 7 − l o w b i t ( 7 ) = 6 7-lowbit(7)=6 7lowbit(7)=6 6 − l o w b i t ( 6 ) = 4 6-lowbit(6)=4 6lowbit(6)=4 4 − l o w b i t ( 4 ) = 0 4-lowbit(4)=0 4lowbit(4)=0,而 C 7 C_7 C7的左兄弟是 C 6 C_6 C6 C 6 C_6 C6的左兄弟是 C 4 C_4 C4,它们合起来就是区间 [ 1 , 7 ] [1,7] [1,7]的和。可以发现,我们求和的操作就是从区间右端点 r r r开始,不断地累加 C r C_r Cr,同时对 r r r作减去 l o w b i t lowbit lowbit操作。那么修改操作呢?看图会发现,对于任意的 A i A_i Ai,它影响到了多个 C j C_j Cj,即从叶子节点到根节点整条路径上的 C j C_j Cj,所以需要逆着来,即从要开始的子节点 i i i开始,不断修改 C i C_i Ci,同时对 i i i做加上 l o w b i t lowbit lowbit操作。有人可能会问,你这求的是 [ 1 , r ] [1,r] [1,r]的和啊,我要求 [ l , r ] [l,r] [l,r]的和怎么办呢?很简单,把 [ 1 , r ] [1,r] [1,r] [ 1 , l − 1 ] [1,l-1] [1,l1]的和均求出来,然后用前者减去后者即可得到答案。

树状数组的基本操作:

这里我以求某段区间和作为例题来介绍一下树状数组的基本操作(注意树状数组的下标是从1开始的)。
l o w b i t lowbit lowbit操作:

inline int lowbit(int x)
{
	return x&(-x);
}

更新操作(单点修改),比如 a i a_i ai加上 v v v

void update(int i,int v)
{
	for(;i<=n;i+=lowbit(i))
		tree[i]+=v;
}

查询操作,比如求 [ 1 , l ] [1,l] [1,l]的和:

int sum(int l)
{
	int s=0;
	for(;l;l-=lowbit(l))
		s+=tree[l];
	return s;
}

注意树状数组的初始化需要把元素逐一插入到树状数组中,所以初始化的复杂度是 O ( n l g n ) O(nlgn) O(nlgn)的。

二维树状数组

举一个简单的例子,我们有16个元素: A 1 、 A 2 、 A 3 、 A 4 、 … … A 16 A1、A2、A3、A4、……A16 A1A2A3A4A16构成了一个4*4的矩阵,我想查询某个矩阵内所有元素的和,并且可能会修改该矩阵的某个元素怎么做呢?朴素算法肯定是遍历统计和,复杂度 O ( n 2 ) O(n^2) O(n2),那有没有 O ( ( l g n ) 2 ) O((lgn)^2) O((lgn)2)的算法呢?有,利用二维树状数组。对于矩阵的每一行,我们用一个树状数组来管理,对于这 n n n个矩阵,我们用一个树状数组来管理。
我们先写出三个一维树状数组:
t r e e 1 [ 1 ] = A 1     t r e e 1 [ 2 ] = A 1 + A 2       t r e e 1 [ 3 ] = A 3       t r e e 1 [ 4 ] = A 1 + A 2 + A 3 + A 4 tree1[1]=A1 \ \ \ tree1[2]=A1+A2 \ \ \ \ \ tree1[3]=A3\ \ \ \ \ tree1[4]=A1+A2+A3+A4 tree1[1]=A1   tree1[2]=A1+A2     tree1[3]=A3     tree1[4]=A1+A2+A3+A4
t r e e 2 [ 1 ] = A 5     t r e e 2 [ 2 ] = A 5 + A 6       t r e e 2 [ 3 ] = A 7       t r e e 2 [ 4 ] = A 5 + A 6 + A 7 + A 8 tree2[1]=A5 \ \ \ tree2[2]=A5+A6 \ \ \ \ \ tree2[3]=A7\ \ \ \ \ tree2[4]=A5+A6+A7+A8 tree2[1]=A5   tree2[2]=A5+A6     tree2[3]=A7     tree2[4]=A5+A6+A7+A8
t r e e 3 [ 1 ] = A 9     t r e e 3 [ 2 ] = A 9 + A 10     t r e e 3 [ 3 ] = A 11     t r e e 3 [ 4 ] = A 9 + A 10 + A 11 + A 12 tree3[1]=A9 \ \ \ tree3[2]=A9+A10 \ \ \ tree3[3]=A11\ \ \ tree3[4]=A9+A10+A11+A12 tree3[1]=A9   tree3[2]=A9+A10   tree3[3]=A11   tree3[4]=A9+A10+A11+A12
……
然后再写出我们的二维树状数组:

T r e e [ 1 ] [ 1 ] = A 1 Tree[1][1]=A1 Tree[1][1]=A1
T r e e [ 1 ] [ 2 ] = A 1 + A 2 Tree[1][2]=A1+A2 Tree[1][2]=A1+A2
T r e e [ 1 ] [ 3 ] = A 3 Tree[1][3]=A3 Tree[1][3]=A3
T r e e [ 1 ] [ 4 ] = A 1 + A 2 + A 3 + A 4 Tree[1][4]=A1+A2+A3+A4 Tree[1][4]=A1+A2+A3+A4

T r e e [ 2 ] [ 1 ] = A 1 + A 5 Tree[2][1]=A1+A5 Tree[2][1]=A1+A5
T r e e [ 2 ] [ 2 ] = A 1 + A 2 + A 5 + A 6 Tree[2][2]=A1+A2+A5+A6 Tree[2][2]=A1+A2+A5+A6
T r e e [ 2 ] [ 3 ] = A 3 + A 7 Tree[2][3]=A3+A7 Tree[2][3]=A3+A7
T r e e [ 2 ] [ 4 ] = A 1 + A 2 + A 3 + A 4 + A 5 + A 6 + A 7 + A 8 Tree[2][4]=A1+A2+A3+A4+A5+A6+A7+A8 Tree[2][4]=A1+A2+A3+A4+A5+A6+A7+A8

T r e e [ 3 ] [ 1 ] = A 9 Tree[3][1]=A9 Tree[3][1]=A9
T r e e [ 3 ] [ 2 ] = A 9 + 10 Tree[3][2]=A9+10 Tree[3][2]=A9+10
T r e e [ 3 ] [ 3 ] = A 11 Tree[3][3]=A11 Tree[3][3]=A11
T r e e [ 3 ] [ 4 ] = A 9 + A 10 + A 11 + A 12 Tree[3][4]=A9+A10+A11+A12 Tree[3][4]=A9+A10+A11+A12

……

也就是说 T r e e [ i ] Tree[i] Tree[i]维护的是: t r e e i + t r e e ( i − 1 ) + … … + t r e e ( i − l o w b i t ( i ) + 1 ) treei+tree(i-1)+……+tree(i-lowbit(i)+1) treei+tree(i1)++tree(ilowbit(i)+1) 比如上面 T r e e [ 2 ] Tree[2] Tree[2]维护的就是 t r e e 2 tree2 tree2 t r e e 1 tree1 tree1
那么我们求这个矩阵从第1行到第x行,第1列到第y列的操作就很简单了:(就是原来的一维变成了二维)。
求中间某个矩阵的和也可以使用类似一维树状数组的思想,把这个矩阵分为四个以左上角为原点的子矩阵。

void add(int x,int y,int v)
{
	for(int i=x;i<=n;i+=lowbit(i))
		for(int j=y;j<=n;j+=lowbit(j))
			tree[i][j]+=v;
}

int query(int x,int y)
{
	int sum=0;
	for(int i=x;i;i-=lowbit(i))
		for(int j=y;j;j-=lowbit(j))
			sum+=tree[i][j];
	return sum;
}

树状数组处理 R M Q RMQ RMQ问题:

修改:

void updata(int x)
{
	int lx,i;
	while(x<=n)
	{
		h[x]=a[x];
		lx=lowbit(x);
		for(i=1;i<lx;i<<=1)
			h[x]=max(h[x],h[x-i]);
		x+=lowbit(x);
	}		
}

查询:

int query(int x,int y)
{
	int ans=0;
	while(y>=x)
        {
		ans=max(a[y],ans);
		y--;
		for(;y-lowbit(y)>=x;y-=lowbit(y))
			ans=max(h[y],ans);
	}
	return ans;
}

针对区间 [ 1 , r ] [1,r] [1r]的查询:

int query(int r)//查询[1,r]
{
    int ans=INF;
    while(r)
    {
        ans=min(ans,tree[r]);
        r-=lowbit(r);
    }
    return ans;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值