线段树初步

线段树初步

通过了一个暑假的学习颓废,本蒟蒻又回来了。今天要介绍的算法是线段树。关于线段树这个高级数据结构,咱们会从三个方面(即”什么是线段树?“、“怎么种线段树?”、“线段树的用途”)来了解。算法


什么是线段树?

线段树的定义:

线段树是一种二叉搜索树,与区间树类似,它将一个区间划分为一些单元区间,每一个单元区间对应线段树中得一个叶结点。——\(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\)才能保证不会越界学习


怎么种线段树?

(搞得像一篇生物学博客)ui

\(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))\)spa

下面的代码创建了一棵线段树而且在每一个节点上保存了对应区间的最大值:code

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\)htm

在线段树中,根节点(编号为\(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\)吧!

相关文章
相关标签/搜索