数据结构-树状数组

功能

维护一个数据结构,对一列n个数,实现下面两种操作:

  1. 将某一个数加上 x
  2. 求出下标从x到y所有数的和

思路

定义 l o w b i t ( i ) lowbit(i) lowbit(i)表示将i转化为二进制后最低位的1所对应的值。比如12的二进制表示为 ( 1100 ) 2 {(1100)}_2 (1100)2,则 l o w b i t ( 12 ) = l o w b i t ( ( 1100 ) 2 ) = ( 100 ) 2 = 4 lowbit(12)=lowbit({(1100)}_2)={(100)}_2=4 lowbit(12)=lowbit((1100)2)=(100)2=4
开一个长度为n的数组s[i],表示从下标i开始前 l o w b i t ( i ) lowbit(i) lowbit(i)项和。
在这里插入图片描述
上图中a数组表示输入的原数组,s为树状数组。则:
s[1]=a[1]
s[2]=a[1]+a[2]
s[3]=a[3]
s[4]=a[1]+a[2]+a[3]+a[4]
s[5]=a[5]
s[6]=a[5]+a[6]
s[7]=a[7]
s[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]
s[9]=a[9]
……
那么树状数组应该怎样建立和维护呢?

建树

我们只需自下而上的遍历上图即可完成建树。

void build()
{
	for (int i=1;i<=n;++i) s[i]=a[i];
	for (int i=1;i<=n;i*=2)
		for (int j=i*2;j<=n;j+=i*2) s[j]+=s[j-i];
}
修改

考虑第x个数的值改变会对树状数组中那些值产生影响。
由s[i]表示从下标i开始前 l o w b i t ( i ) lowbit(i) lowbit(i)项和,即第i-lowbit(i)+1~i项之和。
在这里插入图片描述
观察发现修改x的值只会上图中一条链的值,且该条链中i的下一个元素为i+lowbit(i)。
修改第x个元素的值的操作如下:

  1. 将修改树状数组中第x个元素的值。
  2. 若x+lowbit(x)不超过n,则x+=lowbit(x),重复step1。

举个栗子:
比如我们想要让a[5]加1,则需要依次进行以下操作:
s [ 5 ] + 1 = s [ ( 101 ) 2 ] + 1 = s [ 5 ] + 1 s [ 5 + l o w b i t ( 5 ) ] + 1 = s [ ( 101 ) 2 + l o w b i t ( ( 101 ) 2 ) ] + 1 = s [ ( 101 ) 2 + ( 1 ) 2 ] + 1 = s [ ( 110 ) 2 ] + 1 = s [ 6 ] + 1 s [ 6 + l o w b i t ( 6 ) ] = s [ ( 110 ) 2 + l o w b i t ( ( 110 ) 2 ) ] + 1 = s [ ( 110 ) 2 + ( 10 ) 2 ] + 1 = s [ ( 1000 ) 2 ] + 1 = s [ 8 ] + 1 … … \begin{aligned} s[5]+1 & =s[(101)_2]+1\\ & =s[5]+1\\ s[5+lowbit(5)]+1 & =s[(101)_2+lowbit((101)_2)]+1\\ & =s[(101)_2+(1)_2]+1\\ & =s[(110)_2]+1\\ & =s[6]+1\\ s[6+lowbit(6)] & =s[(110)_2+lowbit((110)_2)]+1\\ & =s[(110)_2+(10)_2]+1\\ & =s[(1000)_2]+1\\ & =s[8]+1\\ & …… \end{aligned} s[5]+1s[5+lowbit(5)]+1s[6+lowbit(6)]=s[(101)2]+1=s[5]+1=s[(101)2+lowbit((101)2)]+1=s[(101)2+(1)2]+1=s[(110)2]+1=s[6]+1=s[(110)2+lowbit((110)2)]+1=s[(110)2+(10)2]+1=s[(1000)2]+1=s[8]+1
代码实现如下:

void change(int x,int y)
{
	for (int i=x;i<=n;i+=lowbit(i))
		s[i]+=y;
}
查询

想要查询下标从x到y的所有元素的和,只需要查询前y项和与前x-1项和作差即可。
统计前x项的和的方法如下:

  1. 将第x-lowbit(x)+1~x个元素的和加入答案,即ans+=s[x]。
  2. 若x-lowbit(x)不为0,则x-=lowbit(x),重复step1。
  3. ans即为所求。

举个栗子:
我们想要统计前82项和,现将82转化为二进制得 ( 1010010 ) 2 {(1010010)}_2 (1010010)2。由上述树状数组定义得:
s [ ( 1010010 ) 2 ] = s [ 82 ] = a [ 82 ] + a [ 81 ] s [ ( 1010010 ) 2 − l o w b i t ( ( 1010010 ) 2 ) ] = s [ ( 1010010 ) 2 − ( 10 ) 2 ] = s [ ( 1010000 ) 2 ] = s [ 80 ] = a [ 80 ] + a [ 79 ] + … … + a [ 66 ] + a [ 65 ] s [ ( 1010000 ) 2 − l o w b i t ( ( 1010000 ) 2 ) ] = s [ ( 1010000 ) 2 − ( 10000 ) 2 ] = s [ ( 1000000 ) 2 ] = s [ 64 ] = a [ 64 ] + a [ 63 ] + … … + a [ 2 ] + a [ 1 ] \begin{aligned} s[{(1010010)}_2] & =s[82]\\ & =a[82]+a[81]\\ s[{(1010010)}_2-lowbit({(1010010)}_2)] & =s[{(1010010)}_2-(10)_2]\\ & =s[(1010000)_2]\\ & =s[80]=a[80]+a[79]+……+a[66]+a[65]\\ s[(1010000)_2-lowbit((1010000)_2)] & =s[(1010000)_2-(10000)_2]\\ & =s[(1000000)_2]\\ & =s[64]\\ & =a[64]+a[63]+……+a[2]+a[1] \end{aligned} s[(1010010)2]s[(1010010)2lowbit((1010010)2)]s[(1010000)2lowbit((1010000)2)]=s[82]=a[82]+a[81]=s[(1010010)2(10)2]=s[(1010000)2]=s[80]=a[80]+a[79]++a[66]+a[65]=s[(1010000)2(10000)2]=s[(1000000)2]=s[64]=a[64]+a[63]++a[2]+a[1]
则前82项和为s[82]+s[80]+s[64]。
代码实现如下。

int find(int x)
{
	int sum=0;
	for (int i=x;i;i-=lowbit(i))
		sum+=s[i];
	return sum;
}
lowbit

定义 l o w b i t ( i ) lowbit(i) lowbit(i)表示将i转化为二进制后最低位的1所对应的值。上文中基于lowbit的定义我们实现了单次操作log(n)的维护上述数据结构。下面给出lowbit(x)的实现。

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

不理解&运算符请搜索C语言位运算。
为什么x&(-x)就是将x转化为二进制后最低位的1所对应的值呢?
这就涉及到了数在计算机中的存储方式。
一个数在计算机中的二进制表示形式叫做这个数的机器数。机器数是带符号的,在计算机用一个数的最高位存放符号,正数符号位为0,负数符号位为1。一个数在计算机中多以二进制补码形式存储。
在介绍补码之前,我们需要先定义原码和反码。

  1. 原码
    现将数的绝对值表示为二进制形式,将最高位按符号置为0/1,就能得到数的原码表示形式。

  2. 反码
    非负数的反码是它本身。
    负数的反码是原码除符号位逐位取反。

  3. 补码
    非负数的补码是它本身。
    负数的补码是反码+1。

举个栗子:
( + 2 ) 10 = ( + 10 ) 2 = ( 00000010 ) 原 码 = ( 00000010 ) 反 码 = ( 00000010 ) 补 码 ( − 2 ) 10 = ( − 10 ) 2 = ( 10000010 ) 原 码 = ( 11111101 ) 反 码 = ( 11111110 ) 补 码 \begin{aligned} (+2)_{10} &=(+10)_2\\ &=(0000 0010)_{原码}\\ &=(0000 0010)_{反码}\\ &=(0000 0010)_{补码}\\ (-2)_{10} &=(-10)_2\\ &=(1000 0010)_{原码}\\ &=(1111 1101)_{反码}\\ &=(1111 1110)_{补码} \end{aligned} (+2)10(2)10=(+10)2=(00000010)=(00000010)=(00000010)=(10)2=(10000010)=(11111101)=(11111110)

由于x为数组下标,下文将x视为非负的int类型变量展开讨论。
当x为0时,0&0=0。
当x不为0时,x为正数,则其补码就是自身的二进制形式。
-x的补码是将反码+1的结果,
( − x ) 反 码 + 1 (-x)_{反码}+1 (x)+1后,
将会对 ( − x ) 反 码 (-x)_{反码} (x)包括最低位0开始右边所有字节逐位取反,
( − x ) 反 码 (-x)_{反码} (x)最低位0左边保持不变。
易知 ( x ) 补 码 (x)_{补码} (x) ( − x ) 反 码 (-x)_{反码} (x)的每一位都不同,
所以 ( − x ) 补 码 (-x)_{补码} (x)包括最低位1开始向右所有字节均与 ( x ) 补 码 (x)_{补码} (x)相同,
( − x ) 补 码 (-x)_{补码} (x)最低位1左边所有字节均与 ( x ) 补 码 (x)_{补码} (x)不同,
则x&(-x)会取出将x转化为二进制后最低位的1所对应的值。

代码

#include <stdio.h>
#define lowbit(x) x&(-x) 
int n,m,s[500001],t,x,y;
void build()
{
	for (int i=1;i<=n;i*=2)
		for (int j=i*2;j<=n;j+=i*2) s[j]+=s[j-i];
}
int find(int x)
{
	int sum=0;
	for (int i=x;i;i-=lowbit(i))
		sum+=s[i];
	return sum;
}
void change(int x,int y)
{
	for (int i=x;i<=n;i+=lowbit(i))
		s[i]+=y;
}
int main()
{
	scanf("%d%d",&n,&m);
	for (int i=1;i<=n;++i) scanf("%d",&s[i]);
	build();
	while (m--)
	{
		scanf("%d%d%d",&t,&x,&y);
		if (t==1) change(x,y);
		else printf("%d\n",find(y)-find(x-1));
	}
	return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值