功能
维护一个数据结构,对一列n个数,实现下面两种操作:
- 将某一个数加上 x
- 求出下标从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个元素的值的操作如下:
- 将修改树状数组中第x个元素的值。
- 若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项的和的方法如下:
- 将第x-lowbit(x)+1~x个元素的和加入答案,即ans+=s[x]。
- 若x-lowbit(x)不为0,则x-=lowbit(x),重复step1。
- 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)2−lowbit((1010010)2)]s[(1010000)2−lowbit((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。一个数在计算机中多以二进制补码形式存储。
在介绍补码之前,我们需要先定义原码和反码。
-
原码
现将数的绝对值表示为二进制形式,将最高位按符号置为0/1,就能得到数的原码表示形式。 -
反码
非负数的反码是它本身。
负数的反码是原码除符号位逐位取反。 -
补码
非负数的补码是它本身。
负数的补码是反码+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;
}