1.什么是线段树?
线段树
(Segment Tree)是一种二叉搜索树,与区间树相似,它将一个区间划分为一些单元区间,每个单元区间对应线段树中的一个叶结点。如下图:一个序列:1,5,4,2,3。把它的区间([1,5])建立成含有编号
和区间
的结点的二叉树,这就是线段树。
2.递归实现线段树的操作
细细地一看,我们会发现一个结点的左子结点的编号为当前结点编号的2倍(2*k或k<<1
,k为当前结点编号),而右结点的编号为当前结点编号的2倍+1(2*k+1或k<<1|1
)。叶子结点的区间的起点等于区间的终点,叶子结点就是我们序列元素了。到这里我们已经基本了解线段树是什么了,但是我们怎么去建立这个线段树呢?
下面我以洛谷的一道题一步步地去了解怎么运用线段树:线段树模板题
有关结点的数组长度
:一般是开到结点值最大*4(maxn<<2)。
sum[maxn<<2]
:记录的每个结点区间的每个数的总和。
arr[maxn]
:记录的是序列元素(1,5,4,2,3)。
void build_tree(int k,int L,int R){///建树,初始为:build_tree(1,1,n);
if(L==R) {///叶子节点,修改
sum[k]=arr[L];///记录sum值,为求和做准备
return;
}
else{
int mid = (L+R)>>1;///中间值
build_tree(k<<1,L,mid);///递归左子树
build_tree(k<<1|1,mid+1,R);///递归右子树
sum[k] = sum[k<<1]+sum[k<<1|1];
///当前结点的区间每个数和:左子树的每个数和+右子树每个数的和
}
}
到这里线段树已经建好了,我们接下来就要了解线段树怎么用,有何用?
点更新
(针对一个数的修改):假设我们把元素3添加2:3+2=5,我们看看那些区间需要修改的。
///[L,R]为原始区间
///[l,r]为要操作的区间
void update(int L,int R,int x,int l,int r,int k){///初始为update(1,n,v,x,y,1);
if(l==r){///到达叶结点
sum[k]+=x;///x为要添加的值
return;
}
int mid = (L+R)>>1;
///判断我们是往左子树调用还是往右子树调用
if(l<=mid)update(L,mid,x,l,r,k<<1);
if(r>mid)update(mid+1,R,x,l,r,k<<1|1);
sum[k] = sum[k<<1]+sum[k<<1|1];
///当前结点区间内每个数的和=左子树区间内每个数的和+右子树区间内每个数的和
}
到这里我们可能对判断往那个方向调用可能疑惑:指定区间分三种情况:
L>=mid:适用第二种情况。R>mid:适用第三种情况。第一种情况l→mid部分交给L>=mid去递归,mid→r部分交给R>mid去递归。
区间更新
(针对指定区间内每个数的修改):假设我们指定区间[2,3]中每个数+1
///lazy[maxn<<2]:标记结点是否更新。
void PushDown(int k,int l,int r){///下推标记
if(lazy[k]){///上一个步骤执行过lazy
lazy[k<<1]+=lazy[k];///其左子结点区间标记
lazy[k<<1|1]+=lazy[k];///其右子结点区间标记
sum[k<<1]+=l*lazy[k];///
sum[k<<1|1]+=r*lazy[k];
lazy[k]=0;///取消当前结点区间的标记
}
}
我们可能会疑问这个lazy数组究竟是有什么用? 我看到很多博客把这个结点叫做延迟结点或懒惰结点,不管它叫啥。我只知道:lazy是为了记忆当前结点区间已经更新了,如果原始区间完全在操作区间内我们就更新到了指定区间了,程序可以结束。我们举个例子:假如我们第一个操作就是要更新的区间为[1,3]
,不断地递归找这个区间,PushDown函数没有执行
(由于lazy数组暂时还是0),当我们找到了,把当前区间结点标记,程序结束。
假设我们第二个操作是更新[1,2]
这个区间,当我们找到这个区间为[1,3]的结点时,我们发现l这个结点的lazy不为0
,这时我们可以执行PushDown函数
,然后当前结点的左结点
和右结点
标记,当前结点取消标记。然后不断递归,只要没有找到,我们继续执行PushDown函数…直到找到这个区间然后才叠加更新,然后标记,程序结束。(到这里应该懂lazy的作用了吧!!!希望对你有帮助)
void update(int L,int R,int x,int l,int r,int k){///区间更新,初始为update(1,n,v,x,y,1);
if(l<=L&&R<=r){///如果原始区间完全在操作区间[l,r]内
sum[k]+=x*(R-L+1);///更新数字和,这就是跟点更新的差别了
lazy[k]+=x;///为了标记当前结点区间添加一个树
return;
}
int mid = (L+R)>>1;
PushDown(k,mid-L+1,R-mid);///下推标记
if(l<=mid)update(L,mid,x,l,r,k<<1);
if(r>mid)update(mid+1,R,x,l,r,k<<1|1);
sum[k] = sum[k<<1]+sum[k<<1|1];
}
区间求和(区间查询)
:
int query(int L,int R,int l,int r,int k){///区间查询求和,初始为query(1,n,x,y,1)
if(l<=L&&R<=r) return sum[k];
int mid = (L+R)>>1;
PushDown(k,mid-L+1,R-mid);///为了保证sum的正确性
ll ans = 0;
if(l<=mid) ans+=query(L,mid,l,r,k<<1);
if(r>mid) ans+=query(mid+1,R,l,r,k<<1|1);
return ans;
}
区间最值
(针对指定区间内所有数的最值):下面以最大值为例。
//segtree[maxn<<2]:记录最值
void build_tree(int k,int L,int R){///建树
if(L==R) {///叶子节点,修改
segtree[k]=arr[L];
return;
}
else{
int mid = (L+R)>>1;///中间值
build_tree(k<<1,L,mid);
build_tree(k<<1|1,mid+1,R);
segtree[k] = max(segtree[k<<1],segtree[k<<1|1]);
///左子树的最值与右子树的最值的最值
}
}
int query(int L,int R,int l,int r,int k){///区间查询求最值
if(l<=L&&R<=r) return segtree[k];
int mid = (L+R)>>1;
ll ans = 0;
if(l<=mid) ans=max(ans,query(L,mid,l,r,k<<1));
if(r>mid) ans=max(ans,query(mid+1,R,l,r,k<<1|1));
return ans;
}
3.非递归实现线段树
能用非递归最好用非递归。再简介之前如果不了解位运算的话可以参考我的另一篇博客:四大运算符:位运算
- 建立线段树:
我们了解了递归地方式后,我们都知道其建树是自上而下
,而非递归方式是自下而上
。
///sum存储区间和
void build_tree(int n){///建树
N=1;
while(N<n+2) N<<=1;///扩大两倍;
for(int i=1;i<=n;i++)
sum[N+i] = arr[i];
for(int i=N-1; i>0; i--){
sum[i] = sum[i<<1]+sum[i<<1|1];
lazy[i]=0;
}
}
- 点更新+区间查询:
我们执行一下程序:求区间[2,4]的和:i=9,j=13。先判断i与j的各父结点不同,只有j可执行添加sum[12]。i=4,j=6,i与j的各父结点不同,只有i可执行,添加sum[5]。i=2,j=3,i与j的父结点相同,程序退出。
void update(int l,int x){
for(int i=N+l;i;i>>=1)
sum[i]+=x;///不断地使其父结点+x
}
///i^j^1值为0时i和j的父结点相同,循环跳出
///我们都知道非位运算~(a)=-(a+1)
///~i&1判断左子树,i为偶返回1
///j&1判断右子树,j为奇返回1
int query(int l,int r){
int ans=0;
for(int i=N+l-1,j=N+r+1;i^j^1;i>>=1,j>>=1 ){
if(~i&1) ans+=sum[i^1];///i^1等同于i+1
if(j&1) ans+=sum[j^1];///j^1等同j-1;
}
return ans;
}
从这里我们可以看出,为什么N要大于等于n+2
了,因为i要从i+1开始,而j要从j-1开始,所以必须要有一个可以作为基准的起点N和终点N+n+1。所以我们存储都是N+1到N+n。
- 区间更新+区间查询:
void update(int l,int r,int x){
int i,j,ln=0,rn=0,count=1;
///ln:i一路走来已经包含了几个数
///rn:j一路走来已经包含了几个数
///count:本层当前结点区间元素个数
for(i=N+l-1,j=N+r+1; i^j^1; i>>=1,j>>=1,count<<=1)
{
sum[i]+=x*ln;
sum[j]+=x*rn;
if(~i&1) lazy[i^1]+=x, sum[i^1]+=x*count, ln+=count;
if(j&1) lazy[j^1]+=x, sum[j^1]+=x*count, rn+=count;
}
///更新指定最上层,i与j共同父结点的地方
for(;i;i>>=1,j>>=1){
sum[i]+=x*ln;
sum[j]+=x*rn;
}
}
int query(int l,int r){
int i,j,ln=0,rn=0,count=1;
int ans = 0;
for(i=N+l-1,j=N+r+1; i^j^1; i>>=1,j>>=1,count<<=1)
{
if(lazy[i]) ans+=lazy[i]*ln;
if(lazy[j]) ans+=lazy[j]*rn;
if(~i&1)ans+=sum[i^1], ln+=count;
if(j&1) ans+=sum[j^1], rn+=count;
}
for(;i;i>>=1,j>>=1){
ans+=lazy[i]*ln;
ans+=lazy[j]*rn;
}
return ans;
}
博主正在不断地学习算法,坚持只要对于每一个算法有新的理解都会不断更新博文的原则,如果有关算法类博文存在不足之处,请大佬们不吝指教!!!