在信息学竞赛中,我们经常需要动态维护一个数列的前缀和信息;这当然可以用 和线段树分块 来实现,但我们还有更轻量级的数据结构 -----> 树状数组。
与线段树类似,树状数组也是用于动态查询区间(其实是“前缀”)信息、支持修改的数据结构。
与线段树相比,它更简洁,实现起来也更方便,但是它能维护的信息比较有限。
树状数组
(
B
i
n
a
r
y
I
n
d
e
x
e
d
T
r
e
e
(
B
.
I
.
T
)
,
F
e
n
w
i
c
k
T
r
e
e
)
(Binary Indexed Tree(B.I.T), FenwickTree)
(BinaryIndexedTree(B.I.T),FenwickTree)是一个查询和修改复杂度都为log(n)的数据结构。
可以实现单点(区间)修改和区间查询
算法思想
那我们如何实现这样一个数据结构呢?
- 任何一个十进制数都可以转化为一个二进制数,这个二进制数的位数是 l o g log log级别的。
- 类似的,我们在求前缀 「 1 , a 」 「1,a」 「1,a」的信息时,我们也希望将其分解成若干个不相交的子集的和。
于是,如下图,一个存放子集的和的树结构就诞生了。
该结构满足以下性质
- 每个节点 c [ x ] c[x] c[x]保存以它为根的子树中所有叶节点的和。
- c[x]的子节点个数 = = l o w b i t ( x ) ==lowbit(x) ==lowbit(x)的位数
- x的父亲节点是 x + l o w b i t ( x ) x+lowbit(x) x+lowbit(x)。
lowbit(x)表示取出x二进制最低位的1及其之后的0表示的数
图中
c
c
c数组方框里的数表示其二进制编码
线表示该子集由以下几个子集得来
例如:
c
[
2
]
=
c
[
1
]
+
a
[
2
]
,
c
[
4
]
=
c
[
2
]
+
c
[
3
]
+
a
[
4
]
c[2]=c[1]+a[2], c[4]=c[2]+c[3]+a[4]
c[2]=c[1]+a[2],c[4]=c[2]+c[3]+a[4]
省略a数组不看,我们发现x的父亲节点的下标是x加上其二进制最后一位的1;
(即x+lowbit(x))
那么已经可以解决单点修改的问题了
假如将x位置上的数加上y
那么我们只需要将x,x的父节点,x的父节点的父节点 … 都加上y就可以了
时间复杂度是log级别的
而不用修改其他节点的值,因为其他节点的值不由x得来;
表示成代码
inline void update(int x,int y)
{
for(;x<=n;x+=lowbit(x)) c[x]+=y;
}
那如何查询前缀和呢?
先看几个例子
- s [ 6 ] = c [ 6 ] + c [ 4 ] − > ( 二 进 制 表 示 ) s [ 110 ] = c [ 110 ] + c [ 100 ] s[6]=c[6]+c[4]-> ( 二进制表示 ) s[110]=c[110]+c[100] s[6]=c[6]+c[4]−>(二进制表示)s[110]=c[110]+c[100]
- s [ 5 ] = c [ 5 ] + c [ 4 ] − > s [ 101 ] = c [ 101 ] + c [ 100 ] s[5]=c[5]+c[4] -> s[101]=c[101]+c[100] s[5]=c[5]+c[4]−>s[101]=c[101]+c[100]
- s [ 13 ] = c [ 13 ] + c [ 12 ] + c [ 8 ] − > s [ 1101 ] = c [ 1101 ] + c [ 1100 ] + c [ 1000 ] s[13]=c[13]+c[12]+c[8] -> s[1101]=c[1101]+c[1100]+c[1000] s[13]=c[13]+c[12]+c[8]−>s[1101]=c[1101]+c[1100]+c[1000]
可以看出每次求前缀和[1,x],从x开始,每次减去下标二进制最后一个1,然后加上
c
[
x
]
c[x]
c[x],到0为止即可;
与修改操作相反,每次减去lowbit(x)
inline int sum(int x)
{
int res=0;
for(;x;x-=lowbit(x)) res+=c[x];
return res;
}
树状数组存储的是前缀和信息,对于前缀信息能否转化成区间信息要加以注意。
以下是代码
1.单点修改+区间查询
1.lowbit
inline int lowbit(int n)
{
return n&(-n);
}
2.单点加法
inline void update(int x,int y)
{
for(;x<=n;x+=lowbit(x)) c[x]+=y;
}
3.查询前缀和
inline int sum(int x)
{
int res=0;
for(;x;x-=lowbit(x)) res+=c[x];
return res;
}
完整代码
#include<iostream>
#include<cstdio>
using namespace std;
int n,m;
const int N=1000010;
int c[N];
inline int lowbit(int n)
{
return n&(-n);
}
inline void update(int x,int y)
{
for(;x<=n;x+=lowbit(x)) c[x]+=y;
}
inline int sum(int x)
{
int res=0;
for(;x;x-=lowbit(x)) res+=c[x];
return res;
}
int main()
{
scanf("%d%d",&n,&m);
int opt,x,k;
int a,b;
for(int i=1;i<=n;i++) {
scanf("%d",&x);
update(i,x);
}
for(int i=1;i<=m;i++) {
scanf("%d%d%d",&opt,&a,&b);
if(opt) update(a,b);
else printf("%d\n",sum(b)-sum(a-1));
}
return 0;
}
2.区间修改+区间查询
首先需要两棵树状数组
用结构体存储
#define LL long long
typedef LL treetype;
struct Bit
{
treetype c[N];
inline int lowbit(int _n)
{
return _n&(-_n);
}
inline void update(int x,treetype y)
{
for(;x<=n;x+=lowbit(x))
c[x]+=y;
return;
}
inline treetype sum(int x)
{
treetype res=0;
for(;x>=1;x-=lowbit(x))
res+=c[x];
return res;
}
};
Bit c1,c2;
利用差分的思想
先将整个数列差分存入c数组
那么
a
[
i
]
=
c
[
1
]
+
c
[
2
]
+
.
.
.
+
c
[
i
]
a[i]=c[1]+c[2]+...+c[i]
a[i]=c[1]+c[2]+...+c[i]; 即
a
[
i
]
=
∑
j
=
1
i
c
[
j
]
a[i]=\sum_{j=1}^{i}c[j]
a[i]=∑j=1ic[j] -------(1)
前缀和
s
u
m
[
i
]
=
∑
j
=
1
i
a
[
j
]
sum[i]=\sum_{j=1}^{i}a[j]
sum[i]=∑j=1ia[j]
s
u
m
[
i
]
=
a
[
1
]
+
a
[
2
]
+
.
.
.
+
a
[
i
]
sum[i]=a[1]+a[2]+...+a[i]
sum[i]=a[1]+a[2]+...+a[i] --------(2)
我们将1式代入2式,得到
s
u
m
[
i
]
=
c
[
1
]
+
(
c
[
1
]
+
c
[
2
]
)
+
.
.
.
+
(
c
[
1
]
+
c
[
2
]
+
.
.
.
+
c
[
i
]
)
sum[i]=c[1]+(c[1]+c[2])+...+(c[1]+c[2]+...+c[i])
sum[i]=c[1]+(c[1]+c[2])+...+(c[1]+c[2]+...+c[i])
=
i
∗
c
[
1
]
+
(
i
−
1
)
∗
c
[
2
]
+
(
i
−
2
)
∗
c
[
3
]
+
.
.
.
+
1
∗
c
[
i
]
=i * c[1]+(i-1)*c[2]+(i-2)*c[3]+...+1 * c[i]
=i∗c[1]+(i−1)∗c[2]+(i−2)∗c[3]+...+1∗c[i]
=
i
∗
(
c
[
1
]
+
c
[
2
]
+
c
[
3
]
+
.
.
.
+
c
[
i
]
)
−
(
0
∗
c
[
1
]
+
1
∗
c
[
2
]
+
.
.
.
+
(
i
−
1
)
∗
c
[
i
]
)
=i * (c[1]+c[2]+c[3]+...+c[i]) - (0*c[1]+1 * c[2]+...+(i-1)*c[i])
=i∗(c[1]+c[2]+c[3]+...+c[i])−(0∗c[1]+1∗c[2]+...+(i−1)∗c[i])
s
u
m
[
i
]
=
i
∗
∑
j
=
1
i
c
[
j
]
−
∑
j
=
1
i
(
j
−
1
)
∗
c
[
j
]
sum[i]=i*\sum_{j=1}^{i}c[j]-\sum_{j=1}^{i}(j-1)*c[j]
sum[i]=i∗∑j=1ic[j]−∑j=1i(j−1)∗c[j]
所以用一个树状数组存
c
[
1
]
−
−
c
[
n
]
c[1]--c[n]
c[1]−−c[n]
另一个树状数组存
(
i
−
1
)
∗
c
[
i
]
(i-1)*c[i]
(i−1)∗c[i]即可
1.区间加法
inline void modify(int x,int y,treetype val)
{
c1.update(x,val);
c1.update(y+1,-val);
c2.update(x,(x-1)*val);
c2.update(y+1,-y*val);
return;
}
2.前缀和查询
inline treetype prefix(int x)
{
return x*c1.sum(x)-c2.sum(x);
}
完整代码
#include<iostream>
#include<cstdio>
#define LL long long
using namespace std;
typedef LL treetype;
const int N=1e5+5;
int n,m;
struct Sgtree
{
struct Bit
{
treetype c[N];
inline int lowbit(int _n)
{
return _n&(-_n);
}
inline void update(int x,treetype y)
{
for(;x<=n;x+=lowbit(x))
c[x]+=y;
return;
}
inline treetype sum(int x)
{
treetype res=0;
for(;x>=1;x-=lowbit(x))
res+=c[x];
return res;
}
};
Bit c1,c2;
inline void modify(int x,int y,treetype val)
{
c1.update(x,val);
c1.update(y+1,-val);
c2.update(x,(x-1)*val);
c2.update(y+1,-y*val);
return;
}
inline treetype prefix(int x)
{
return x*c1.sum(x)-c2.sum(x);
}
};
Sgtree t;
int main()
{
scanf("%d%d",&n,&m);
LL opt,x,y,k;
for(int i=1;i<=n;i++)
{
scanf("%lld",&x);
t.modify(i,i,x);
}
while(m--)
{
scanf("%lld%lld%lld",&opt,&x,&y);
if(opt==1)
{
scanf("%lld",&k);
t.modify(x,y,k);
}
else
printf("%lld\n",t.prefix(y)-t.prefix(x-1));
}
return 0;
}
3.二维树状数组
如同一维的树状数组,把二维的树状数组定义如下
s
u
m
[
i
]
[
j
]
=
∑
i
=
x
−
c
(
x
)
+
1
x
∑
j
=
y
−
c
(
y
)
+
1
y
a
[
i
]
[
j
]
sum[i][j]=\sum_{i=x-c(x)+1}^{x}\sum_{j=y-c(y)+1}^{y}a[i][j]
sum[i][j]=∑i=x−c(x)+1x∑j=y−c(y)+1ya[i][j]
其实加一层循环就可以了
#include<iostream>
#include<cstdio>
#define LL long long
using namespace std;
const int N=5000+5;
int c[N][N];
int n,m;
typedef LL treetype;
inline int lowbit(int n)
{
return n&(-n);
}
inline void update(int x,int y,treetype key)
{
for(int i=x;i<=n;i+=lowbit(i))
for(int j=y;j<=m;j+=lowbit(j))
c[i][j]+=key;
return;
}
inline treetype sum(int x,int y)
{
treetype res=0;
for(int i=x;i;i-=lowbit(i))
for(int j=y;j;j-=lowbit(j))
res+=c[i][j];
return res;
}
int main()
{
return 0;
}
推荐两位大佬的博客
友情链接