(广东工业大学 ACM 寒假集训 专题五 A B)
树状数组的概念
顾名思义,树状的数组(bushi)
前置知识lowbit运算
先看,可能不知道有什么用,等一下就知道了。
比如
l
o
w
b
i
t
(
1100
)
=
(
100
)
2
=
4
lowbit(1100)=(100)_{2}=4
lowbit(1100)=(100)2=4
l
o
w
b
i
t
(
1110
)
=
(
10
)
2
=
2
lowbit(1110)=(10)_{2}=2
lowbit(1110)=(10)2=2
l
o
w
b
i
t
(
100
)
=
(
100
)
2
=
4
lowbit(100)=(100)_{2}=4
lowbit(100)=(100)2=4
我们来看这样一个问题:
如题,已知一个数列,你需要进行下面两种操作:
1.将某一个数加上 x
2.求出某区间每一个数的和
考虑朴素算法,我们使用普通数组储存,那么我们第一个操作只需要
O
(
1
)
O(1)
O(1),但是第二个操作情况最坏是
O
(
n
)
O(n)
O(n),如果我们进行好多好多次这样的操作,第二个操作很可能超时。所以我们使用树状数组来储存数列,使得第二个操作的复杂度降为
O
(
l
o
g
2
n
)
O(log_{2}{n})
O(log2n),不过第一个操作的复杂度也升高了。
这里是一个树状数组
t
r
e
e
tree
tree,每个元素以长条方式显示。比如
t
r
e
e
[
8
]
tree[8]
tree[8]就是原数组从下标1到8的前缀和,
t
r
e
e
[
12
]
tree[12]
tree[12]就是原数组下标从9到12的前缀和,
t
r
e
e
[
9
]
tree[9]
tree[9]就是原数组下标9。
看起来这个数组挺复杂的,不过摸清规则就很简单了。
进一步,再来看这张图,这张图很明显能看出树的样子,父节点代表所有子节点的和。
比如
t
[
8
]
=
t
[
4
]
+
t
[
6
]
+
t
[
7
]
t[8]=t[4]+t[6]+t[7]
t[8]=t[4]+t[6]+t[7]
我们先不考虑代码实现,我们先看如果这样一个树状数组,我们怎么做到刚刚提到的操作:
1.将某一个数加上 x
2.求出某区间每一个数的和
操作1: 比如我们给 a [ 3 ] a[3] a[3]加上 k k k,因为 t [ 3 ] , t [ 4 ] , t [ 8 ] t[3],t[4],t[8] t[3],t[4],t[8]都有 a [ 3 ] a[3] a[3]在里面,所以我们要给 t [ 3 ] , t [ 4 ] , t [ 8 ] t[3],t[4],t[8] t[3],t[4],t[8]加 k k k。那么怎么找到下标加上 k k k呢, l o w b i t lowbit lowbit运算就派上用场了,当前下标加 l o w b i t ( i ) lowbit(i) lowbit(i)就能得到下一个下标的位置,只要从3开始循环到末尾(不大于原数组长度)就算成功修改了。
操作2:如果要求7之前的元素的和,我们需要把
t
[
7
]
,
t
[
6
]
,
t
[
4
]
t[7],t[6],t[4]
t[7],t[6],t[4]加起来,就能得到前缀和。
操作2是操作一的反向,当前下标减
l
o
w
b
i
t
(
i
)
lowbit(i)
lowbit(i)就能得到下一个下标的位置。
在b站找到一个比较好的视频,详细请看那里。
树状数组有关操作模板
1、lowbit操作
int lowbit(int x){
return x&(-x);
}
2、单点修改
void add(int i,int k){
while(i<=n){
tree[i]+=k;
i+=lowbit(i);
}
}
3、前缀和查询(区间查询)
int search(int i){
int sum=0;
while(i!=0){
sum+=tree[i];
i-=lowbit(i);
}
return sum;
}
1和2的运用:洛谷P3374
(广东工业大学 ACM 寒假集训 专题五 A)
题目描述
如题,已知一个数列,你需要进行下面两种操作:
将某一个数加上 x
求出某区间每一个数的和
输入格式
第一行包含两个正整数 n,m,分别表示该数列数字的个数和操作的总个数。
第二行包含 n 个用空格分隔的整数,其中第 i个数字表示数列第 i 项的初始值。
接下来 m 行每行包含 3 个整数,表示一个操作,具体如下:
1 x k 含义:将第 x 个数加上 k
2 x y 含义:输出区间 [x,y] 内每个数的和
输出格式
输出包含若干行整数,即为所有操作 22 的结果。
输入输出样例
#include<iostream>
using namespace std;
//int p[500005];
int n,m,tree[500005];//n数的个数
int lowbit(int x){
return x&(-x);
}
void add(int i,int a){//单点增加
while(i<=n){
tree[i]+=a;
i+=lowbit(i);
}
}
int search(int i){//前缀和查询
int ans=0;
while(i!=0){
ans+=tree[i];
i-=lowbit(i);
}
return ans;
}
int main()
{
int a,b,c;
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++){
int tmp;
scanf("%d",&tmp);
add(i,tmp);
}
while(m--){
scanf("%d %d %d",&a,&b,&c);
if(a==1){
add(b,c);
}
else if(a==2){
cout<<search(c)-search(b-1)<<endl;
}
}
}
下面两个需要差分数组思维,详细这里不说了,上面的视频有讲。
4、区间修改和单点查询
(广东工业大学 ACM 寒假集训 专题五 B)
洛谷P3368
#include<iostream>
using namespace std;
int a[500005],tree[500005];
int t,x,y,z,n,m;
int lowbit(int x){
return x&(-x);
}
void add(int i,int k){
while(i<=n){
tree[i]+=k;
i+=lowbit(i);
}
}
int search(int i){
int sum=0;
while(i!=0){
sum+=tree[i];
i-=lowbit(i);
}
return sum;
}
int main()
{
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
}
while(m--){
scanf("%d",&t);
if(t==1){
scanf("%d %d %d",&x,&y,&z);
add(x,z);
add(y+1,-z);
}
else if(t==2){
scanf("%d",&x);
cout<<a[x]+search(x)<<endl;
}
}
}