线段树初步
线段树初步
经过了一个暑假的学习颓废,本蒟蒻又回来了。今天要介绍的算法是线段树。关于线段树这个高级数据结构,我们会从三个方面(即”什么是线段树?“、“怎么种线段树?”、“线段树的用途”)来了解。
什么是线段树?
线段树的定义:
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分为一些单元区间,每个单元区间对应线段树中得一个叶结点。——\(by\)百度百科
再详细一点就是说:
线段树是一种基于分治思想的二叉树结构,用于在区间上进行信息统计。与按照二进制位(\(2\)的次幂)进行区间划分的树状数组相比,线段树是一种更加通用的结构
线段树的性质:
- 线段树的每个节点都代表一个区间。
- 线段树具有唯一的根节点,代表的区间是整个统计范围,如\([1,N]\)。
- 线段树的每个叶节点都代表一个长度为\(1\)的元区间\([x,x]\)。
- 对于每个内部节点\([l,r]\),他的左子节点是\([l,mid]\),右子节点是\([mid+1,r]\),其中\(mid=\lfloor(l+r)/2\rfloor\)
这样一来,我们就能简单的使用一个\(struct\)数组来保存线段树。当然,如果树的最后一层节点在数组中保存的位置不是连续的,直接空出数组中多余的位置即可。在理想情况下,\(N\)个叶节点的满二叉树有\(N+N/2+N/4+...+2+1=2N-1\)个节点。因为在上述存储方式下,最后一层产生了空余,所以保存线段树的数组长度要不小于\(4N\)才能保证不会越界
怎么种线段树?
(搞得像一篇生物学博客)
\(Step\space 1\) 建树:
线段树的基本用途是对序列进行维护,支持查询与修改指令。给定一个长度为\(N\)的序列\(A\),我们可以在区间\([1,N]\)上建立一颗线段树,当节点的左端点等于右端点时,节点\([i,i]\)保存\(A[i]\)的值。线段树的二叉树结构可以很方便的从上到下传递信息。以区间最大值问题为例,记\(dat(l,r)\)等于\(max_{l\leq i\leq r}\{A[i]\}\),显然\(dat(l,r)=max(dat(,l,mid),dat(mid+1,r))\)。
下面的代码建立了一棵线段树并且在每个节点上保存了对应区间的最大值:
struct Tree{
int l;//存储该节点的左端点
int r;//存储该节点的右端点
int mid;//存储该节点左儿子和右儿子的分界线
int ans;//存储区间[l,r]的最大值
}tree[maxn*4];//数组开四倍
void build(int l,int r,int p){//表示以区间[l,r]为根节点,且当前节点的编号为p建立一棵(子)树
tree[p].l=l,tree[p].r=r;
tree[p].mid=(l+r)>>1;//计算出中间的分界线
if(l==r){
tree[p].ans=a[l];//如果当前节点为叶子节点,该节点的值就为数列对应位置的值
}
build(l,tree[p].mid,p<<1);//构建左子树
build(tree[p].mid+1,r,p<<1|1);//构建右子树
tree[p].ans=max(tree[p>>1].ans,tree[p>>1|1].ans);//在确定完左儿子和右儿子的值后,用这两个的值来更新当前的答案值
}
build(1,n,1);//调用入口
\(Step\space 2\) 线段树的单点修改:
单点修改是一条形如”\(C\space x\space v\)“的指令,表示把\(A[x]\)的值修改为\(v\)。
在线段树中,根节点(编号为\(1\)的节点)是执行各种指令的入口。我们需要从根节点出发,递归找到代表区间\([x,x]\)的节点,然后从下往上更新\([x,x]\)以及它的所有祖先节点上保存的信息。
下面代码中的函数执行了修改当前节点权值:
void update(int p,int x,int v){//表示修改到点p,要求将A[x]修改为v
if(tree[p].l==tree[p].r){
tree[p].ans=v;
return ;//如果到了区间[x,x],将这个节点的值修改为v
}
if(x<=tree[p].mid){
update(p<<1,x,v);
} else{
update(p<<1|1,x,v);
}//根据x与分界点的mid的关系确定要递归的区间
tree[p].ans=max(tree[p<<1].ans,tree[p<<1|1].ans);//在确定完左儿子和右儿子的值后,用这两个的值来更新当前的答案值
}
\(Step\space 3\) 线段树的区间查询:
区间查询是一条形如"\(Q\space l\space r\)"的指令,例如查询序列\(A\)在区间\([l,r]\)上的最大值,即\(max_{l\leq i\leq r}\{A[i]\}\)。我们只需要从根节点开始,递归执行以下过程:
\(1.\)若\([l,r]\)完全覆盖了当前节点所表示的区间,则立即回溯,并且该节点的\(ans\)值为候选答案。
\(2.\)若左子节点与\([l,r]\)有重叠部分,则递归访问左子节点。
\(3.\)若右子节点与\([l,r]\)有重叠部分,则递归访问右子节点。
下面代码中的函数执行了区间查询:
int query(int p,int l,int r){
if(l<=tree[p].l&&tree[p].r<=r){
return t[p].ans;
}
int now=-(1<<30);
if(l<=tree[p].mid){
now=max(now,query(p<<1,l,r));
}
if(r>tree[p].mid){
now=max(now,query(p<<1|1,l,r));
}
}
printf("%d",query(1,l,r));
至此,线段树已经能像\(ST\)算法一样处理区间最值问题,并且还支持动态修改某个数的值。同时,线段树也已经能支持树状数组单点增加与查询前缀和的指令,接下来就是更加高级的操作了。
延迟标记:
在此通俗的解释我理解的\(Lazy\)意思,比如现在需要对\([a,b]\)区间值进行加\(c\)操作,那么就从根节点\([1,n]\)开始调用\(update\)函数进行操作,如果刚好执行到一个子节点,它的节点标记为\(p\),这时$tree[p].l == a && tree[p].r == b \(这时我们可以一步更新此时\)p\(节点的\)sum[p]\(的值,\)sum[p] += c * (tree[p].r - tree[p].l + 1)\(,注意关键的时刻来了,如果此时按照常规的线段树的\)update\(操作,这时候还应该更新\)p\(子节点的\)sum[]\(值,而\)Lazy\(思想恰恰是暂时不更新\)rt\(子节点的\)sum[]\(值,到此就\)return\(,直到下次需要用到rt子节点的值的时候才去更新,这样避免许多可能无用的操作,从而节省时间 。 ——\)yicbs$
线段树的用途?
线段树可以应用与很多情况,对于一些比较简单的模拟题目,可以用线段树切掉(当然只有大佬会这做);
大部分树状数组可以解决的问题,线段树都能解决,而且会更快一点;
还有就是区间上符合结合律的(如加法、异或),并且含有修改、查询等操作的题目。
好了,接下来就是刷题时间了,一起\(AC\)吧!