浅谈线段树
线段树(长,但是好用!)
问题
给你一个长度为 \(n\)的序列\(A_1,A_2,A_3…A_n\)进行如下操作:
1.对这个序列的某个点加上一个数字vol
2.求[l,r]区间的数字和
看到这里,键盘侠就要开始喷了:你这打着线段树的名号讲树状数组的**(和谐)东西
不慌,再来看第三条:
3.求[l,r]区间的最大值和最小值
这样的话,树状数组就不好做了吧(蒟蒻得意)
那么,进入正题,线段树!!!
线段树好不好用
根据某巨所言:傻逼线段树又臭又长!线段树真快真好用!
啥叫线段树?
回到上面这个问题,求区间和的操作用树状数组很容易就能维护,可是求区间最大最小……
好像树状数组就不灵了?!这个时候,线段树英勇地站了出来
首先,针对一个区间,我们可以人为地把这个区间进行划分,比如[1,5]就可以变成[1,3][4,5]两个区间合并的结果
那么,每次把一个区间进行二分,就……就得到了一棵二叉树!
[1,5]
[1,3][4,5]
[1,2][3,3][4,4][5,5]
[1,1][2,2]
void build(int p,int l,int r)
{
if(l==r)return ;//叶结点
int mid=(l+r) / 2;
build(p*2, l, mid),build(p*2+1, mid+1, r);
}
//线段树建树模板
线段树怎么用?
既然我们已经有了一个个区间了,那么我们就要做点有意义的事情
Such as:开一个sum前缀和数组,mx最大值数组,mn最小值数组
可是,这些数组该怎么维护呢?
这一句空空如也,盘他!
if(l==r)
{
sum[p]=a[l];
mx[p]=a[l];
mn[p]=a[l];
return ;
}
这时,蒟蒻脑袋里蹦出了一个problem:树的叶结点改变,那父结点呢?
这可怎么办?
不慌,因为线段树是二叉树结构,所以我们直接提取ta儿子的信息就行了
void update(int p)
{
sum[p]=sum[p*2]+sum[p*2+1];
mx[p]=max(mx[p*2],mx[p*2+1]);
mn[p]=min(mn[p*2],mn[p*2+1]);
}
单点修改
现在知道维护了以后,那么就要开始第一个操作:修改单点值
修改单点的思想其实就是搜索\(l=r=x\)的一个叶结点,但是要注意修改后的区间信息要重新丢掉update()里面去维护一下
void change(int p,int l,int r,int x,int v)
{
if(l==r)
{
sum[i]+=v;
return ;
}
int mid=(l+r)/2;
if(x<=mid)change(p*2,l,mid,x,v);
else change(p*2+1,mid+1,r,x,v);
update(p);
}
//单点修改前缀和模板
区间查询
看了上面这么多,有一个致命的问题:区间没有全列举到!!!
wow,这个就很难办,要是无法处理,线段树彻底成为废物
but区间是可以拆分和合并的……
还是那个[1,5]的区间,现在我们要拿出[1,4]这个区间的信息:
[1,5]
[1,3][4,5]
[1,2][3,3][4,4][5,5]
[1,1][2,2]
显然我们要用的是线段树中[1,3][4,4]两个节点,他们又都是[1,4]的一部分……
等等?![1,4]的一部分!!!(垂死病中惊坐起)
int query(int p,int l,int r,int x,int y)
//[x,y]是我们要找的区间
{
if(l>=x&&r<=y)
return sum[p];//调取的信息
int mid=(l+r)/2,ans;//ans代表这个区间某个值(前缀和,最大最小值等……)
if(x<=mid)ans+=query(p*2,l,mid,x,y);
if(y>mid)ans+=query(p*2+1,mid+1,r,x,y);
return ans;
}
//前缀和调取模板
------------恢复内容结束------------