目录
前言
首先提出一个问题:对于一个长度为n的序列,进行以下操作:
- 给下标为x的位置的数加上c,简称 modify
- 求区间 [ L, R ] 的和 ,简称 query
有下列几种方法可以实现
- 利用数组实现操作,modify的时间复杂度为O(1),query的时间复杂度为O(N)
- 利用前缀和数组,modify的时间复杂度为O(N),query的时间复杂度为O(1)
如果题中要求进行m次modify或者query操作,那么时间复杂度就会变成O(n*m),很多情况下会超时;于是,树状数组站出来了;
树状数组对这两种操作的时间复杂度进行了折中处理
- modify复杂度为O(log n)
- query 复杂度为O(log n)
原理
任何一个 十进制数 都能用 二进制数 来表示
eg: X= ()
那么区间 [1,x] 可以划分为以下几个连续的区间:
- (x- , x] 区间长度为
- (x-- , x-] 区间长度为
- (x--- , x--] 区间长度为
- .........
- (x---...--- , x--...--- ] 区间长度为
由上述区间可知,每个区间的长度就是区间右端点R的 lowbit(R) (就是R的二进制的最低为的1所代表的2次幂)
所以上述任何一个区间 ( L , R ] 都可以表示为 [R-lowbit(R)+1, R]
设 c[R] 为 以 R 结尾,长度是 的区间和 (就是lowbit(R)) 则可得以下树状数组的图解(其中下方为原数组a的下标)
query
拿求区间 sum=[1,12] 的和举个例子;由图例可知 sum= c[12] + c[8]
又因为8=12-lowbit(12),所以 sum= c[12] + c[12-lowbit(12)]
于是推出区间求和操作 sum=[1,R]= c[R] + c[R-lowbit(R)] + c [ temp - lowbit(temp)]+.... (temp=R-lowbit(R))
int query(int x)//查询区间1~x的区间和;
{
int res=0;
for(int i=x;i>=1;i-=lowbit(i)) res+=tr[i];
return res;
}
modify
那么更改位置x的数值以后,怎样更新区间的数值呢?
eg:更新a[5]位置 的值,执行操作a[5]+=c;
- 首先a[5]这个位置要加上c,也就是 c[5]+=c
- 然后找子节点和父节点对应关系:c[5]对应的父节点为 c[6] ,c[6]对应的父节点为c[8], c[8]对应的父节点为c[16]
- 也就是 5 ——> 6 ——> 8 ——> 16
- 转化关系为 6 =5+lowbit(5) ; 8 =6+lowbit(6) ; 16=8+lowbit(8)
所以子节点更新父节点的方式就是,每次加上自身的lowbit
void modify(int x,int c)//修改树状数组x位置的值
{
for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
}
上述就是树状数组的基本原理,对于由父节点找子节点,由子节点找父节点的二进制原理,以及lowbit的基本原理,这里没有介绍;感兴趣的同学可以去b站找视频或者csdn等了解下;
模板
int lowbit(int x)
{
return x&-x;
}
void modify(int x,int c)//修改树状数组x位置的值
{
for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
}
int query(int x)//查询区间1~x的区间和;
{
int res=0;
for(int i=x;i>=1;i-=lowbit(i)) res+=tr[i];
return res;
}
实际作用
简单说下树状数组能解决的问题:
- 单点修改,区间求和(利用模板即可)
- 区间修改,单点查询(做差分树状数组)
- 区间修改,区间求和(做差分树状数组,做前缀树状数组)
(能用树状数组解决的问题都能用线段树解决,树状数组原理难理解,但代码简单;线段树原理简单,代码长)