树状数组
特点
树状数组 c [ x ] c[x] c[x] 维护序列 a a a 的区间 [ x − l o w b i t ( x ) + 1 , x ] [x - lowbit(x) + 1 , x] [x−lowbit(x)+1,x]
lowbit(x) = x & -x;
性质 (摘自小蓝书)
- c [ x ] c[x] c[x]保存以它为根的子树中所有叶节点的信息和
- c [ x ] c[x] c[x]的儿子节点(含它本身)的个数等于 l o w b i t ( x ) lowbit(x) lowbit(x) 后的位数。
- 除根节点外,每个内部节点 c [ x ] c[x] c[x] 的父节点是 c [ x + l o w b i t ( x ) ] c[x + lowbit(x)] c[x+lowbit(x)]
- 树的深度为 O ( l o g N ) O(logN) O(logN)
基操
- 单点修改
- 单点查询
代码
单点修改
void add(int x,int y) {
for(; x <= n; x += x & -x) c[x] += y;
//x + lowbit(x)遍历的是x的父节点,利用了性质3
}
单点查询
int ask(int x) {
int ans = 0;
for(; x; x -= x & -x) ans += c[x];
return ans;
//x - lowbit(x)遍历的是x的兄弟节点,结合上图理解
}
推荐练习
升级操作
逆序对
逆序对问题简单来说就是求形如 i < j i < j i<j 且 a [ i ] > a [ j ] a[i] > a[j] a[i]>a[j] 的个数。
在序列
a
a
a 的数值范围上建立树状数组,初始化全为零。
倒序扫描给定的序列
a
a
a ,对于每个数
a
[
i
]
a[i]
a[i]:
- 在树状数组中查询前缀和 [ 1 , a [ i ] − 1 ] [1,a[i] - 1] [1,a[i]−1] ,累加到答案中。
- 执行单点修改操作时,把 c [ a [ i ] ] c[a[i]] c[a[i]] 的值 + 1 +1 +1,表示 a [ i ] a[i] a[i] 又出现了一次同时维护前缀和。
int ans = 0;
for(int i = n; i; i--) {
ans += ask(a[i] - 1);
add(a[i],1);
}
如果数值范围过大可以对数据进行离散化,注意值域需为正数。
原理
在倒序扫描时,已经出现过的数就是在 a [ i ] a[i] a[i] 后面的数,所以树状数组查询的内容就是在 a [ i ] a[i] a[i] 后比它小的数有多少个,与问题等价。
推荐练习
注意数据范围
#include<cstdio>
#include<cstring>
const int N = 2e5;
int left[N + 5],right[N + 5],c[N + 5],a[N + 5],n;
void add(int x,int y) {
for(; x <= N; x += x & -x) c[x] += y;
}
int ask(int x) {
int ans = 0;
for(; x; x -= x & -x) ans += c[x];
return ans;
}
inline int read() {
int x = 0,flag = 1;
char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-')flag = -1;ch = getchar();}
while(ch >='0' && ch <='9'){x = (x << 3) + (x << 1) + ch - 48;ch = getchar();}
return x * flag ;
}
long long ans = 0;
int main() {
n = read();
for(int i = 1; i <= n; i++) a[i] = read();
for(int i = 1; i <= n; i++) {
left[i] = ask(N) - ask(a[i]);
add(a[i],1);
}
memset(c,0,sizeof(c));
for(int i = n; i >= 1; i--) {
right[i] = ask(N) - ask(a[i]);
add(a[i],1);
}
for(int i = 1; i <= n; i++) ans += 1ll * left[i] * right[i];
printf("%lld ",ans);
memset(c,0,sizeof(c));
ans = 0;
for(int i = 1; i <= n; i++) {
left[i] = ask(a[i] - 1);
add(a[i],1);
}
memset(c,0,sizeof(c));
for(int i = n; i >= 1; i--) {
right[i] = ask(a[i] - 1);
add(a[i],1);
}
for(int i = 1; i <= n; i++) ans += 1ll * left[i] * right[i];
printf("%lld",ans);
return 0;
}
区间增加 + 单点查询
思路:变区间增加为单点增加。
维护一个新数组
b
b
b 的前缀和,
b
[
1
,
x
]
b[1,x]
b[1,x] 表示
a
[
x
]
a[x]
a[x] 的变化量。
将
C
C
C
l
l
l
r
r
r
d
d
d 变为
b
[
l
]
b[l]
b[l] 加上
d
d
d,
b
[
r
+
1
]
b[r + 1]
b[r+1] 减去
d
d
d。
原理很好想,就不赘述了。
查询答案时将查询到的值加上原值就可以了。
这种把维护区间转换成维护区间变化的思路特别重要。
推荐练习
#include<cstdio>
const int N = 1e5 + 5;
int a[N],c[N],n,m;
void add(int x,int y) {
for(; x <= n; x += x & -x) c[x] += y;
}
int ask(int x) {
int ans = 0;
for(; x; x -= x & -x) ans += c[x];
return ans;
}
int main() {
scanf("%d%d",&n,&m);
for(int i = 1; i <= n; i++) scanf("%d",&a[i]);
for(int i = 1; i <= m; i++) {
char ord[2];
scanf("%s",ord);
if(ord[0] == 'C') {
int l,r,d; scanf("%d%d%d",&l,&r,&d);
add(l,d);
add(r + 1,-d);
}
else {
int x; scanf("%d",&x);
printf("%d\n",a[x] + ask(x));
}
}
return 0;
}
区间增加 + 区间查询
详见小蓝书(算法竞赛进阶指南)P207 ~P209。
推荐练习
#include<cstdio>
const int N = 1e5 + 5;
int a[N],n,m;
long long c[2][N],sum[N];
void add(int i,int x,int y) {
for(; x <= n; x += x & -x) c[i][x] += y;
}
long long ask(int i,int x) {
long long ans = 0;
for(; x; x -= x & -x) ans += c[i][x];
return ans;
}
int main() {
scanf("%d%d",&n,&m);
for(int i = 1; i <= n; i++) {
scanf("%d",&a[i]);
sum[i] = sum[i - 1] + a[i];
}
for(int i = 1,l,r; i <= m; i++) {
char ord[2];
scanf("%s%d%d",ord,&l,&r);
if(ord[0] == 'C') {
int d; scanf("%d",&d);
add(0,l,d);
add(0,r + 1,-d);
add(1,l,l * d);
add(1,r + 1,-(r + 1) * d);
}
else {
long long ans = sum[r] + (r + 1) * ask(0,r) - ask(1,r);
ans -= sum[l - 1] + l * ask(0,l - 1) - ask(1,l - 1);
printf("%lld\n",ans);
}
}
return 0;
}
推荐练习
由易到难,做完所有的推荐练习,你就能熟练掌握树状数组了。
板子题1
板子题2
最接近神的人(一道阅读理解题,可以借这道题练一下“值域树状数组”的离散化。)
谜一样的牛
方差(虽然这道题线段树更好写,但可以尝试一下树状数组的写法。)
尾声
树状数组作为进阶数据结构中较为简单的结构之一,其作用更多的是帮助维护其它数据结构。