先说一下这两个东西的作用;
树状数组的作用有局限性,其大部分是用来动态求前缀和的,而线段树可以说是吃百家饭的,可以动态地求出一个区间的性质,比如一个区间中的最值和总和。
那么线段树就一定比树状数组优秀吗?从功能上看是这样的,但是树状数组的用代码实现的复杂度是线段树无法比拟的。
那么先讲一下树状数组吧。
由于这个东西的证明十分复杂,所以这边就省得绕晕各位了。
有兴趣的可以由我给出的函数的功能推出具体的证明,这个是行得通的。
来一张具体的树状数组的图;
其中一个下标的二进制后缀0的个数为k个时,这个数就在第k层
第一个函数 lowbit
void lowbit(int x){//返回2^k,k是x二进制的后缀0的个数
return x&-x;
}
第二个函数 query 返回1~x的前缀和,时间复杂度是log(n);
int query(int x){
int sum=0;
for(int i=x;i!=0;i-=lowbit(i)) sum+=tr[i]; //tr[i]为树中下标为
return sum;
}
第三个函数 modify 在第k个值上加w 时间复杂度是log(n);
void modify(int x,int w){
for(int i=x;i<=n;i+=lowbit(i))
tr[x]-=w;
}
这就是树状数组的查询和改数操作;
那么下面是线段树了,这个数据结构就很复杂了,一般可用到4个函数,分别是更新节点、 构建树、查询树、修改树,四个操作,其底层实现的原理是dfs;
我们需要一个结构体来构建树;
struct node{
int l,r;
int sum;
}tr[N];//tr[i]的保存着原数组l~r区间的和,其子节点是tr[i<<1]和tr[i<<1|1];
这边以维护前缀和为例
第一个函数 push_up 更新节点函数
void push_up(int x){
tr[x].sum=tr[x<<1].sum+tr[x<<1|1].sum;//tr[x]的子节点是tr[x<<1]和re[x<<1|1],等价于x*2和x*2+1;
}
第二给函数 build 创建一个区间的线段树; 时间复杂度nlog(n)
void build(int x,int l,int r){//x为当前节点的下标,l、r为当前节点的区间端点
if(l==r){
tr[x]={l,r,st[r]};//st[r]存储着原数组中下标为r的数
}else{
tr[x]={l,r};//讲tr[x].l和tr[x].r更新
int mid=l+r>>1;
build(x<<1,l,mid); build(x<<1|1,mid+1,r); //递归创建子节点
push_up(x); //创建完两个子节点后由两个子节点更新父节点
}
}
第三个函数 query 询问l~r的区间和 时间复杂度log(n);
int query(int x,int l,int r){//x为当前树中点的下标,l、r为所求区间的端点
if(tr[x].l>=l&&tr[x].r<=r)//如果这个节点所维护的区间为所求区间的子集,那么我们直接返回这个节点的sum
return tr[x].sum;
else{
int sum=0;
int mid=tr[x].l+tr[x].r>>1;
if(l<=mid) sum+=query(x<<1,l,r);//筛选于所求区间有交集的集合
if(r>=mid+1) sum+=query(x<<1|1,l,r);//筛选于所求区间有交集的集合
return sum;
}
}
第4个函数 modify 修改某个数;
void modify(int x,int p,int w){//下为当前点在树中的下标,p为需在原数组中修改的位置,w为需要加上的值
if(tr[x].l==tr[x].r){//此刻tr[x]为存储位置为p~p的前缀和
tr[x].sum-=w;
return;
}else{
int mid=tr[x].l+tr[x].r>>1;
if(p<=mid) modify(x<<1,p,w);//寻找p所在区间的位置
else modify(x<<1|1,p,w);
push_up(x);//因为p点的数字改变,所以得更新树
}
}
大概就是这几个函数,一定要结合上面的图来理解,要不然会晕掉;