前置知识
位运算(在学习这个算法之前,请务必熟练掌握位运算)
前缀和(利用了前缀和的思想,需要掌握前缀和思想)
解决问题
除主席树以外几乎所有问题都能够用树状数组解决,不过十分麻烦
树状数组模板所解决的问题如下:
给出一个n个数的数组和m次操作
每次操作可以将数组中的某个元素加上一个值
或者需要你输出某个区间的元素之和
在这里用LGOJ3374 【模板】树状数组1
来作为题面
算法详解
最下层是原数组,上面的长得像一颗树的东西就是树状数组
对于原数组a[]={1,2,3,4,5,6,7,8,9,10,11,12,13}
可以生成树状数组c[]={1,3,3,10,5,11,7,36,9,19,11,42,13}
生成树状数组的规则
将树状数组下标用二进制来表示出来就是:
0001,0010,0011,0100,0101,0110,0111,1000,1001,1010,1011,1100
树状数组生成规则1:
第一个数0001
的末尾0个数为0,也就是没有末尾0,那么c[1]=a[1]
第二个数0010
的末尾0个数为1,那么c[2]=a[1]+a[2]
第三个数0011
的末尾0个数为0,那么c[3]=a[3]
第四个数0100
的末尾0个数为2,那么c[4]=a[1]+a[2]+a[3]+a[4]
也就是树状数组中如果某一个下标为x
,将x
转化为二进制之后的末尾0个数为y
,
那么
c
[
x
]
=
a
[
x
]
+
a
[
x
−
1
]
+
⋯
+
a
[
x
−
2
y
+
1
]
c[x]=a[x]+a[x-1]+\dots+a[x-2^y+1]
c[x]=a[x]+a[x−1]+⋯+a[x−2y+1]
这就是树状数组的生成规则
用计算机的思考方式来生成树状数组
末尾0个数
首先我们需要快速判断一个数转化成二进制的末尾0个数
如果你对位运算十分地熟悉的话,请将数值代入到下面的公式进行计算:
x&(-x)
你算完后会很神奇地发现这个公式的计算结果的末尾0个数,就是x
转化为二进制后的末尾0的个数
如果x
的末尾0个数为y
的话,这个公式的计算结果就会是
2
y
2^y
2y
这个公式我们用lowbit()
函数来表示
即
int lowbit(int x)
{
return x&(-x);
}
快速相加
我们现在已经知道了每个数的末尾0个数,但是不能说是某个数有4个末尾0,就真的把原数组中
2
4
=
16
2^4=16
24=16个元素全部算一遍,这样太浪费时间了
这个时候我们又需要用上lowbit()
函数了
看看这个图片,你会发现c[4]
所连接的元素,是c[2],c[3],a[4]
三个元素,而不是a[1],a[2],a[3],a[4]
四个元素
是因为c[3]
的值就是a[1]+a[2]
所以我们只用调用c[3]
一个值而不是a[1],a[2]
两个值,这样我们就节省下来了时间
这个操作我们可以用lowbit()
函数来实现
void add(int x, int y)//将c[x]加上y
{
while(x<=n)
{
c[x]+=y;
x+=lowbit(x);
}
}
这个用语言不大好解释,需要自己跟着这个代码推演一边,只要认真推演了就可以完全理解并掌握这个数据结构的核心了。
前缀和
我们可以发现,其实树状数组的每一个元素都是原数组某一段区间的区间和
那么很多的区间凑到一起,就可以凑出一个原数组的前缀和来
int getsum(int x, int y)
{
int t=0;
while(x>0)
{
t+=c[x];
x-=lowbit(x);
}
return t;
}
同样,自己跟着代码认认真真,仔仔细细地推演一遍
得到了前缀和,区间和也呼之欲出了
主程序
int main()
{
cin>>n>>m;
for(int i=1,x;i<=n;i++)
{
cin>>x;
add(i,x);//在输入的时候就要边输入边生成树状数组
}
while(m--)
{
int cse,x,y;
cin>>cse>>x>>y;
if(cse==1)
add(x,y);//单点修改
else
cout<<getsum(y)-getsum(x-1)<<endl//区间查询
}
}