21.3.2 树状数组 总结
作者:互联网
树状数组
特点
树状数组 \(c[x]\) 维护序列 \(a\) 的区间 \([x - lowbit(x) + 1 , x]\)
lowbit(x) = x & -x;
性质 (摘自小蓝书)
- \(c[x]\)保存以它为根的子树中所有叶节点的信息和
- \(c[x]\)的儿子节点(含它本身)的个数等于 \(lowbit(x)\) 后的位数。
- 除根节点外,每个内部节点 \(c[x]\) 的父节点是 \(c[x + lowbit(x)]\)
- 树的深度为 \(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\) 且 \(a[i] > a[j]\) 的个数。
在序列 \(a\) 的数值范围上建立树状数组,初始化全为零。
倒序扫描给定的序列 \(a\) ,对于每个数 \(a[i]\):
- 在树状数组中查询前缀和 \([1,a[i] - 1]\) ,累加到答案中。
- 执行单点修改操作时,把 \(c[a[i]]\) 的值 \(+1\),表示 \(a[i]\) 又出现了一次同时维护前缀和。
int ans = 0;
for(int i = n; i; i--) {
ans += ask(a[i] - 1);
add(a[i],1);
}
如果数值范围过大可以对数据进行离散化,注意值域需为正数。
原理
在倒序扫描时,已经出现过的数就是在 \(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[1,x]\) 表示 \(a[x]\) 的变化量。
将 \(C\) \(l\) \(r\) \(d\) 变为 \(b[l]\) 加上 \(d\),\(b[r + 1]\) 减去 \(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
最接近神的人(一道阅读理解题,可以借这道题练一下“值域树状数组”的离散化。)
谜一样的牛
方差(虽然这道题线段树更好写,但可以尝试一下树状数组的写法。)
尾声
树状数组作为进阶数据结构中较为简单的结构之一,其作用更多的是帮助维护其它数据结构。
标签:单点,21.3,树状,int,查询,add,数组 来源: https://www.cnblogs.com/sjzyh/p/14826245.html