博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
线段树模板
阅读量:5266 次
发布时间:2019-06-14

本文共 4955 字,大约阅读时间需要 16 分钟。

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点,每个节点所储存的是一个区间的信息。

假设有编号从1到n的n个点,每个点都存了一些信息,用[L,R]表示下标从L到R的这些点。

线段树的用处就是,对编号连续的一些点进行修改或者统计操作,修改和统计的复杂度都是O(log2(n)).

线段树的原理,就是,将[1,n]分解成若干特定的子区间(数量不超过4*n),然后,将每个区间[L,R]都分解为

少量特定的子区间,通过对这些少量子区间的修改或者统计,来实现快速对[L,R]的修改或者统计。

一.基本概念及其形状

1.使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,实际应用时一般还要开4N的数组以免越界,因此有时需要离散化让空间压缩。

2.每个区间包括以下几个部分:
(1)区间左端点,右端点(必须有)
(2)根据题目信息来实现维护修改之类的(根据实际情况而定)

3.线段树的基本思想:二分

4.线段树的一般结构:

由上图可以看出每个叶子节点的区间都是[L,mid],[mid+1,R],L代表上一个区间的左端点,R代表上一个区间的右端点,mid代表上面左右区间的中间值。所以线段树是很有规律的。

二.解决题目的基本操作

1.首先一般求解树的问题,都是先要建树。线段树也不例外,建树一般都是模板的。如下:

const int maxn=10000; //一般为点的总个数 int a[maxn+5],st[maxn+5];int sum[maxn*4+1];void pushup(int rt){    sum[rt]=sum[rt*2]+sum[rt*2+1];  //这里是求和的pushup,如果求max就改成sum[rt]=max(sum[rt*2],sum[rt*2+1]这个样子。} //用来把当前节点信息更新到父节点 (求和更新) void build(int l,int r,int rt){  //rt表示当前子树的根,也就是当前所在的节点     if(l==r){     //如果左端点等于右端点,则说明到了底端叶子节点则返回         st[rt]=a[l];        return ;     }    int m=(l+r)/2;    build(l,m,rt*2);  //构建左子树     build(m+1,r,rt*2+1); //构建右子树     pushup(rt); }

 2.建好树了后对于题目就会有一系列的要求,比如单点替换(更新),求区间最值,求区间和之类的

点修改:

void update(int l,int r,int p,int add,int rt){ // add为要增加的数,p代表判断要更新的数字,     if(l==r){        sum[rt]+=add;  //如果是叶节点则修改         return ;    }    int m=(l+r)/2;    if(p<=m) update(l,m,p,add,rt*2); //左边l,右边m     else update(m+1,r,p,add,rt*2+1); //左边m+1,右边 r     pushup(rt); }

区间求和:

int query(int L,int R,int l,int r int rt){ //[L,R]表示操作区间,[l,r]表示当前区间,rt表示当前节点编号     if(L<=l && R>=r){ //在区间内直接返回         return sum[rt];    }    int m=(l+r)/2;  //左子区间:[l,m] 右子区间:[m+1,r] 求和区间:[L,R]     int ans;    if(L<=m) ans+=query(L,R,l,m,rt*2); //左子区间与[L,R]有重叠,递归     if(R>m)  ans+=query(L,R,m+1,r,rt*2+1); //右子区间与[L,R]有重叠,递归     return ans;}

 

三.区间方面的线段树

在做区间修改的时候,有个叫懒惰标记(延迟标记)。

根据大佬们的解释,标记的含义:
代表了这个结点的值已经被更新过了, 但是没有进行子树的结点值更改操作, 用lazy数组标记一下.
所以, 每次进行值的更新和查询操作, 每到一个有 lazy 标记的结点, 必须进行向下更新.

即,如果要给一个区间的所有值都加上1,那么,实际上并没有给这个区间的所有值都加上1,而是打个标记,记下来,这个节点所包含的区间需要加1.打上标记后,要根据标记更新本节点的统计信息,比如,如果本节点维护的是区间和,而本节点包含5个数,那么,打上+1的标记之后,要给本节点维护的和+5。这是向下延迟修改,但是向上显示的信息是修改以后的信息,所以查询的时候可以得到正确的结果。有的标记之间会相互影响,所以比较简单的做法是,每递归到一个区间,首先下推标记(若本节点有标记,就下推标记),然后再打上新的标记,这样仍然每个区间操作的复杂度是O(log2(n))。

标记有相对标记和绝对标记之分:

相对标记是将区间的所有数+a之类的操作,标记之间可以共存,跟打标记的顺序无关(跟顺序无关才是重点)。
所以,可以在区间修改的时候不下推标记,留到查询的时候再下推。
      注意:如果区间修改时不下推标记,那么PushUp函数中,必须考虑本节点的标记。
                 而如果所有操作都下推标记,那么PushUp函数可以不考虑本节点的标记,因为本节点的标记一定已经被下推了(也就是对本节点无效了)
绝对标记是将区间的所有数变成a之类的操作,打标记的顺序直接影响结果,
所以这种标记在区间修改的时候必须下推旧标记,不然会出错。

注意,有多个标记的时候,标记下推的顺序也很重要,错误的下推顺序可能会导致错误。

之所以要区分两种标记,是因为非递归线段树只能维护相对标记。

因为非递归线段树是自底向上直接修改分成的每个子区间,所以根本做不到在区间修改的时候下推标记。
非递归线段树一般不下推标记,而是自下而上求答案的过程中,根据标记更新答案。

所以来说,做普通的线段树单点更新区间查询的时候用不到这个懒惰标记,而做区间更新修改的时候就要加点东西了,而且上面的有些函数也需要加一点东西了。

而且看大佬们一般都用位运算好想说位运算比普通的乘除快,所以基本的先熟悉一下,rt<<1代表位左移一位,就是乘2,rt>>1代表位右移一位,就是除2, rt | 1 就代表加1。

区间更新与点更新不同,它需要更深层次的理解,最为基本的Add(L, R, V)与Query(L,R)系列,然后以此为基础可以衍射出其他各种做法:例如Set(L, R, V)系列就是其衍射品,Set系列更改的就是在查询的时候读取到Lazy标记就立刻返回,而Add系列不同,他要一直往下读它的Lazy标记。

 

首先是pushdown的模板:

void pushdown(int rt,int l,int r){  //向下传递lazy标记     if(lazy[rt]){  //懒惰标记存在         ll le=(r+l>>1)-l+1;  //左子树的长度        ll re=r-l+1-le;      //右子树的长度         lazy[rt<<1]+=lazy[rt];        lazy[rt<<1|1]+=lazy[rt];        sum[rt<<1]+=lazy[rt]*le;        sum[rt<<1|1]+=lazy[rt]*re;        lazy[rt]=0;   //标记清零     } }

然后是查找,如果从根节点开始往下查找符合区间的点,那么久排除(L,R)区间之外的点,但如果此时的根节点范围过大,又有lazy标记,那么就同时要把lazy标记往下移动。

ll query(int L,int R,int l,int r,int rt){    if(L<=l&&R>=r){        return sum[rt];    }    pushdown(rt,l,r);    int m=(l+r)>>1;    ll ret=0;j    if(L<=m) ret+=query(L,R,lson);    if(m

最后是区间更新,更新从根向下,若是(L,R)包含了这个根所在的区间,就直接给这个区间附上lazy标记并且返回,否则就说明有冗余的东西,就得把目前这个点的lazy也下推,然后在新的区间继续找新的值。

void update(int L,int R,int c,int l,int r,int rt){

if(L<=l&&r<=R){
lazy[rt]+=c; //要对sum求和,不然更新父节点值会少
sum[rt]=c*(r-l+1);
return ;
}
int m=(l+r)>>1;
pushdown(rt,l,r); //说明这个根节点的范围不在(L,R)范围内,要递归到下面去继续找
if(L<=m) update(L,R,c,lson);
if(R>m) update(L,R,c,rson);
sum[rt]=sum[rt<<1]+sum[rt<<1|1];
}

 

最后是大佬的永久化标记代码,反正还不怎么懂。

//标记永久化(永久化标记)void push(int rt,int l,int r)       //就是pushudown的作用{    if(lazy[rt]==0)return;    ll le = (r+l>>1)-l+1;    ll re = r-l+1-le;    lazy[rt<<1] += lazy[rt];    lazy[rt<<1|1] += lazy[rt];    sum[rt<<1] += lazy[rt]*le;    sum[rt<<1|1] += lazy[rt]*re;    lazy[rt] = 0;}ll query(int l,int r,int rt,int ql,int qr){    if(ql<=l&&qr>=r) return sum[rt];    else    {        //push(rt,l,r);        int mid = l + r>>1;        ll ret = 1ll*(min(qr,r)-max(ql,l)+1) * lazy[rt];        if(ql<=mid) ret += query(l,mid,rt<<1,ql,qr);        if(qr>mid) ret += query(mid+1,r,rt<<1|1,ql,qr);        return ret;    }}void update(int l,int r,int rt,int ql,int qr,int v){    if(ql<=l&&qr>=r) sum[rt] += 1LL*(r-l+1)*v,lazy[rt] += v;    else    {        int mid = l+r>>1;        if(ql<=mid) update(l,mid,rt<<1,ql,qr,v);        if(qr>mid) update(mid+1,r,rt<<1|1,ql,qr,v);        sum[rt] = sum[rt<<1] + sum[rt<<1|1] + lazy[rt]*(r-l+1);    }

 

 (以上内容基本上为各种博客所看到大佬们的知识,我只是按照自己学习顺序各种找一下..)

 

 

 

转载于:https://www.cnblogs.com/wushengyang/p/10293130.html

你可能感兴趣的文章
[android](学习笔记6)为应用程序添加对话框(1)
查看>>
windows下mongodb安装与使用
查看>>
rotate the clock
查看>>
bugku 变量
查看>>
Python 环境傻瓜式搭建 :Anaconda概述
查看>>
数据库01 /Mysql初识以及基本命令操作
查看>>
数据库02 /MySQL基础数据类型以及多表之间建立联系
查看>>
Python并发编程04/多线程
查看>>
CF461B Appleman and Tree
查看>>
CF219D Choosing Capital for Treeland
查看>>
杂七杂八的小笔记本
查看>>
51Nod1353 树
查看>>
CF1215E Marbles
查看>>
.net Core 图片验证码 基于SkiaSharp实现
查看>>
fish redux 个人理解
查看>>
java 笔记一些
查看>>
BZOJ2339 HNOI2011卡农(动态规划+组合数学)
查看>>
octave基本操作
查看>>
排球计分程序重构(一)
查看>>
axure学习点
查看>>