树状数组和线段树都是用于处理动态区间问题的数据结构。
树状数组:支持区间加法的同时区间查询区间和,以及最值;
线段树:支持区间加法的同时区间乘法的同时区间查询区间和,以及最值。
线段树的适用范围相较于树状数组更加广泛,但树状数组相对于线段树简洁很多,且常数极小。
树状数组的空间复杂度为O(n),时间上单次查询为O(logn)。
首先我们需要知道这个定义:
lowbit(x):表示x在二进制下从低位往高位数的第一个1(最低位有效1)
int lowbit(int x){return x&-x;}
树状数组听起来是一个树形结构,但是实际上用一个长度与操作数组同样为n的数组存储就行了
我们令一个树状数组为t[n],则t[x]表示的是从数组下标为x-lowbit(x)+1到x之间数组元素的和
∴对于单点更新:
void update(int pos,int val){
while(pos<=n)
t[pos]+=val,pos+=lowbit(pos);
}
那么如何求区间和呢?
我们先设sum初始化为0,用于记录数组下标为1~pos的元素之和
树状数组最需要我们理解的一点,就是t[x]表示a[x-lowbit(x)+1]至a[x]的和
那么如果我们要求数组下标为1~pos的元素之和,是不是每次让sum加上t[pos]后,pos变为(pos-lowbit(pos)+1)-1,即pos=pos-lowbit(pos)就可以了?
因为t[x]表示t[x-lowbit(x)+1]到t[pos]的和,每次让pos跳转到这个区间的前一位((x-lowbit(x)+1)-1=x-lowbit(x))就可以了?
计算l~r的和的话,简单的容斥一下:query(r)-query(l-1) 就可以了。
int query(int pos){
int sum=0;
while(pos)
sum+=t[pos],pos-=lowbit(pos);
return sum;
}
例题 树状数组1
如题,已知一个数列,你需要进行下面两种操作:
将某一个数加上 x
求出某区间每一个数的和
第一行包含两个正整数 n,m,分别表示该数列数字的个数和操作的总个数。
第二行包含 n 个用空格分隔的整数,其中第 i 个数字表示数列第 i 项的初始值。
接下来 m 行每行包含 3 个整数,表示一个操作,具体如下:
1 x k
含义:将第 x 个数加上 k
2 x y
含义:输出区间 [x,y] 内每个数的和
输出包含若干行整数,即为所有操作 2 的结果。
代码如下:
#include
using namespace std;
typedef long long ll;
const int maxn=500001;
ll n,m,x,a,b,t[maxn];
ll lowbit(ll x){return x&-x;}
void update(ll pos,ll val){
while(pos<=n)
t[pos]+=val,pos+=lowbit(pos);
}
ll query(ll pos){
ll sum=0;
while(pos)
sum+=t[pos],pos-=lowbit(pos);
return sum;
}
int main()
{
scanf("%lld%lld",&n,&m);
for(ll i=1;i<=n;++i)
{
scanf("%lld",&x);
update(i,x);
}
while(m--){
scanf("%lld%lld%lld",&x,&a,&b);
switch(x){
case 1:
update(a,b);
break;
default:
printf("%lld\n",query(b)-query(a-1));
break;
}
}
return 0;
}
线段树的空间复杂度>=2*n,通常需要4*n避免RE,单次查询时间复杂度为O(logn)
第一次听起来非常容易理解:这是一棵树:树上任意一点都有t[x].l与t[x].r,此节点表示数组下标为t[x].l到t[x].r的元素之和
(1,8)
(1,4) (5,8)
(1,2) (3,4)