引入
二叉索引树,也叫树状数组是一种便于数组单点修改和区间求和的数据结构
主要根据下标的lowbit值来建树
至于lowbit(x),则是(x)&(-(x)),也就是一个二进制数从右边数第一个1代表的数
#define lowbit(x) ((x)&(-(x)))
基础树状数组如下图所示
灰色结点为树状数组中的结点,不难发现,lowbit值相同的结点在同一层上,lowbit值越大越靠近根
我们用一个c数组来存储图中白色长条(最下面结点所代表的区间和就是其本身)的区间和
不难看出
c
i
=
∑
j
=
i
−
l
o
w
b
i
t
(
i
)
i
a
j
c_i=\sum_{j=i-lowbit(i)}^ia_j
ci=j=i−lowbit(i)∑iaj
在有了以上理论基础后,我们来研究一下树状数组中的相关操作
树状数组的相关操作
单点修改
当我们要修改树状数组中的其中一个结点时,我们不仅需要改这个单点的值,还要修改c数组的值(也就是白色长条所代表的值),我们发现,这个结点只有其正上方的白色长条覆盖着,所以只需要一边往上爬一边修改沿路的c就可以了,如图所示
代码
void add(int x,int k){
while(x<=n){
c[x]+=k;//对沿路的c数组进行修改
x+=lowbit(x);//往上爬,若是这一操作无法理解的话,推荐读者打一下草
}
}
求前缀和
为了求一段前缀和,我们可以在图中发现我们需要往左上一直爬并加上沿路的c数组
如图所示
代码
int query(int x){
int sum=0;
while(x>0){
sum+=c[x];//加上沿路的c数组
x-=lowbit(x);//往左上爬
}
return sum;
}
区间修改与区间和
接下来讲解的就是树状数组中比较难理解的区间操作
区间操作需要的是差分
注:接下来的操作推荐读者与笔者一同推算
设
d
i
f
1
=
a
1
,
d
i
f
2
=
a
2
−
a
1
,
d
i
f
3
=
a
3
−
a
2
.
.
.
d
i
f
i
=
a
i
−
a
i
−
1
dif_1=a_1,dif_2=a_2-a_1,dif_3=a_3-a_2...dif_i=a_i-a_{i-1}
dif1=a1,dif2=a2−a1,dif3=a3−a2...difi=ai−ai−1
特别地,
a
0
=
0
a_0=0
a0=0
如果用dif表示出a来,那么可以发现
a
i
=
∑
j
=
1
i
d
i
f
j
a_i=\sum_{j=1}^idif_j
ai=∑j=1idifj
所以得出前缀和
s
u
m
i
=
∑
j
=
1
i
∑
k
=
1
k
d
i
f
k
sum_i=\sum_{j=1}^i\sum_{k=1}^kdif_k
sumi=∑j=1i∑k=1kdifk
=
(
d
i
f
1
)
+
(
d
i
f
1
+
d
i
f
2
)
+
.
.
.
+
(
d
i
f
1
+
d
i
f
2
+
.
.
.
+
d
i
f
i
)
=(dif_1)+(dif_1+dif_2)+...+(dif_1+dif_2+...+dif_i)
=(dif1)+(dif1+dif2)+...+(dif1+dif2+...+difi)
=
i
∗
d
i
f
1
+
(
i
−
1
)
∗
d
i
f
2
+
.
.
.
+
1
∗
d
i
f
i
=i*dif_1+(i-1)*dif_2+...+1*dif_i
=i∗dif1+(i−1)∗dif2+...+1∗difi
=
i
∗
(
d
i
f
1
+
d
i
f
2
+
.
.
.
+
d
i
f
i
)
−
(
0
∗
d
i
f
1
+
1
∗
d
i
f
2
+
.
.
.
(
i
−
1
)
∗
d
i
f
i
)
=i*(dif_1+dif_2+...+dif_i)-(0*dif_1+1*dif_2+...(i-1)*dif_i)
=i∗(dif1+dif2+...+difi)−(0∗dif1+1∗dif2+...(i−1)∗difi)
=
i
∗
∑
j
=
1
i
d
i
f
j
−
i
∗
∑
j
=
1
i
(
j
−
1
)
∗
d
i
f
j
=i*\sum_{j=1}^idif_j-i*\sum_{j=1}^i(j-1)*dif_j
=i∗∑j=1idifj−i∗∑j=1i(j−1)∗difj
然后分别用树状数组T1,T2存这两项
那么在区间修改时,如何对T1,T2修改呢?举个例子
对序列
1
,
2
,
3
,
4
,
5
1,2,3,4,5
1,2,3,4,5的[2,4]进行+1操作
操作后的序列为
1
,
3
,
4
,
5
,
5
1,3,4,5,5
1,3,4,5,5
原序列的查分序列为
1
,
1
,
1
,
1
,
1
1,1,1,1,1
1,1,1,1,1
而操作后的查分序列为
1
,
2
,
1
,
1
,
0
1,2,1,1,0
1,2,1,1,0
可以看到,只需要更改第
l
l
l项和第
r
+
1
r+1
r+1项即可,再对应到T1,T2的具体含义中,就可以进行修改操作了
区间[l,r]和
s
u
m
r
−
s
u
m
l
−
1
sum_r-sum_{l-1}
sumr−suml−1
=
[
r
∗
∑
j
=
1
r
d
i
f
j
−
r
∗
∑
j
=
1
r
(
j
−
1
)
∗
d
i
f
j
]
−
[
(
l
−
1
)
∗
∑
j
=
1
l
−
1
d
i
f
j
−
(
l
−
1
)
∗
∑
j
=
1
l
−
1
(
j
−
1
)
∗
d
i
f
j
]
=[r*\sum_{j=1}^rdif_j-r*\sum_{j=1}^r(j-1)*dif_j]-[(l-1)*\sum_{j=1}^{l-1}dif_j-(l-1)*\sum_{j=1}^{l-1}(j-1)*dif_j]
=[r∗j=1∑rdifj−r∗j=1∑r(j−1)∗difj]−[(l−1)∗j=1∑l−1difj−(l−1)∗j=1∑l−1(j−1)∗difj]
代码
struct BIT{
static const int M=1e5+5;
int c[M];
void add(int x,int k) { while(x<=n) c[x]+=k,x+=lowbit(x); }
int query(int x) { int sum=0;while(x>0) sum+=c[x],x-=lowbit(x);return sum; }
}T1,T2;
//区间修改
void build(int x,int dif){
T1.add(x,dif);
T2.add(x,(x-1)*dif);
}
void interval_add(int l,int r,int k){
T1.add(l,k),T1.add(r+1,-k);
T2.add(l,k*(l-1)),T2.add(r+1,-k*(r+1-1));
}
//区间查询
void interval_query(int l,int r){
return (r*T1.query(r)-(l-1)*T1.query(l-1))-(T2.query(r)-T2.query(l-1));
}
例题
P3374 【模板】树状数组 1
思路
这题的数据还是比较水的,所以笔者直接暴力用前缀和过了
代码
#include<bits/stdc++.h>
using namespace std;
const int M=1e6+5;
#define int long long
#define lowbit(x) (x)&(-(x))
int n,m;
int c[M];
void add(int x,int k) { while(x<=n) c[x]+=k,x+=lowbit(x); }
int query(int x) { int sum=0;while(x>0) sum+=c[x],x-=lowbit(x);return sum; }
signed main()
{
cin>>n>>m;
for(int i=1,p;i<=n;i++) cin>>p,add(i,p);
while(m--){
int if_case;cin>>if_case;
int x,y,k;
switch (if_case){
case 1:cin>>x>>k;add(x,k);break;
case 2:cin>>x>>y;cout<<query(y)-query(x-1)<<endl;break;//暴力前缀和做法
}
}
return 0;
}
P3368 【模板】树状数组 2
思路
这个题就需要区间操作了,需要的是区间修改
我们因为T1和T2存的都是查分,所以需要加一点操作
a i = ∑ j = 1 i d i f j a_i=\sum_{j=1}^idif_j ai=∑j=1idifj
所以查询第x个数时直接输出T1的前缀和就行
理论存在,实践开始
代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define lowbit(x) ((x)&(-(x)))
int n,m;
struct BIT{
static const int M=5e5+5;
int c[M];
void add(int x,int k) { while(x<=n) c[x]+=k,x+=lowbit(x); }
int query(int x) { int sum=0;while(x>0) sum+=c[x],x-=lowbit(x);return sum; }
}T1,T2;
void build(int x,int dif) { T1.add(x,dif),T2.add(x,(x-1)*dif); }
void interval_add(int l,int r,int k){
T1.add(l,k),T1.add(r+1,-k);
T2.add(l,k*(l-1)),T2.add(r+1,-k*(r+1-1));
}
int query(int x) { return T1.query(x); }
signed main()
{
cin>>n>>m;
int a,c=0;
for(int i=1;i<=n;i++){
cin>>a;
c=a-c;
build(i,c);
c=a;
}
while(m--){
int if_case;
cin>>if_case;
int x,y,k;
switch (if_case){
case 1:cin>>x>>y>>k;interval_add(x,y,k);break;
case 2:cin>>x;cout<<query(x)<<endl;break;
}
}
}
end
参考资料:刘汝佳·《算法竞赛入门经典训练指南》
就这样,树状数组讲解完了,笔者不多赘述
完结撒花