线段树 详解

线段树详解

线段树的学习顺序。

单点修改+区间查询

区间修改+区间查询(懒惰标记)

区间合并( 最后的例题就是 )

扫描线,和区间修改很相似,但是没有懒惰标记,难理解一点

最后就是 主席树了,一个很大的不同就是 每个节点的左右儿子并不是简单的 rt<<1和rt<<1|1。

线段树 还是要多想想才能真正理解。这篇只有前两种线段树,区间合并不难,扫描线和主席树还要网上自己再找找。

最后给出一些例题。

一:综述

 

由此看出,用线段树统计的东西,必须符合区间加法,否则,不可能通过分成的子区间来得到[L,R]的统计结果。

符合区间加法的例子:

数字之和——总数字之和 = 左区间数字之和 + 右区间数字之和

最大公因数(GCD)——总GCD = gcd( 左区间GCD , 右区间GCD );

最大值——总最大值=max(左区间最大值,右区间最大值)

不符合区间加法的例子:

众数——只知道左右区间的众数,没法求总区间的众数

01序列的最长连续零——只知道左右区间的最长连续零,没法知道总的最长连续零

 

初学者可以先看这篇文章: 线段树从零开始

 

二:原理

(注:由于线段树的每个节点代表一个区间,以下叙述中不区分节点和区间,只是根据语境需要,选择合适的词)

线段树本质上是维护下标为1,2,..,n的n个按顺序排列的数的信息,所以,其实是“点树”,是维护n的点的信息,至于每个点的数据的含义可以有很多,

在对线段操作的线段树中,每个点代表一条线段,在用线段树维护数列信息的时候,每个点代表一个数,但本质上都是每个点代表一个数。以下,在讨论线段树的时候,区间[L,R]指的是下标从L到R的这(R-L+1)个数,而不是指一条连续的线段。只是有时候这些数代表实际上一条线段的统计结果而已。

 

 

线段树是将每个区间[L,R]分解成[L,M]和[M+1,R] (其中M=(L+R)/2 这里的除法是整数除法,即对结果下取整)直到 L==R 为止。 

开始时是区间[1,n] ,通过递归来逐步分解,假设根的高度为1的话,树的最大高度为(n>1)。

线段树对于每个n的分解是唯一的,所以n相同的线段树结构相同,这也是实现可持久化线段树的基础。

下图展示了区间[1,13]的分解过程:

 

上图中,每个区间都是一个节点,每个节点存自己对应的区间的统计信息。

 

(1)线段树的点修改:

 

假设要修改[5]的值,可以发现,每层只有一个节点包含[5],所以修改了[5]之后,只需要每层更新一个节点就可以线段树每个节点的信息都是正确的,所以修改次数的最大值为层数

复杂度O(log2(n))


(2)线段树的区间查询:

 

线段树能快速进行区间查询的基础是下面的定理:

定理:n>=3时,一个[1,n]的线段树可以将[1,n]的任意子区间[L,R]分解为不超过个子区间。

这样,在查询[L,R]的统计值的时候,只需要访问不超过个节点,就可以获得[L,R]的统计信息,实现了O(log2(n))的区间查询。

下图是n=16 , L=2 , R=15 时的操作图,此图展示了达到最小上界的树的结构。

 

 

 

 

 

(3)线段树的区间修改:

线段树的区间修改也是将区间分成子区间,但是要加一个标记,称作懒惰标记。

标记的含义:

本节点的统计信息已经根据标记更新过了,但是本节点的子节点仍需要进行更新。

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

 

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

相对标记是将区间的所有数+a之类的操作,标记之间可以共存,跟打标记的顺序无关(跟顺序无关才是重点)。

所以,可以在区间修改的时候不下推标记,留到查询的时候再下推。

      注意:如果区间修改时不下推标记,那么PushUp函数中,必须考虑本节点的标记。

                 而如果所有操作都下推标记,那么PushUp函数可以不考虑本节点的标记,因为本节点的标记一定已经被下推了(也就是对本节点无效了)

绝对标记是将区间的所有数变成a之类的操作,打标记的顺序直接影响结果,

所以这种标记在区间修改的时候必须下推旧标记,不然会出错。

 

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

区间修改对应最基本的区间查询,比如:区间求和

为什么可以用懒惰标记?

当你在查询一个区间的时候,一定是从根节点遍历下来的,所以他的懒惰标记一定会被下推到你

要查询的区间,是不会错的。

(4)线段树的存储结构:

线段树是用数组来模拟树形结构,对于每一个节点R ,左子节点为 2*R (一般写作R<<1)右子节点为 2*R+1(一般写作R<<1|1)

然后以1为根节点,所以,整体的统计信息是存在节点1中的。

这么表示的原因看下图就很明白了,左子树的节点标号都是根节点的两倍,右子树的节点标号都是左子树+1:

线段树需要的数组元素个数是:,一般都开4倍空间,比如: int A[n<<2];

 

 

三:递归实现

以下以维护数列区间和的线段树为例,演示最基本的线段树代码。

#define maxn 100007  //元素总个数  
#define ls l,m,rt<<1  
#define rs m+1,r,rt<<1|1  
int Sum[maxn<<2],Add[maxn<<2];//Sum求和,Add为懒惰标记   
int A[maxn],n;//存原数组数据下标[1,n]   

 

(1)建树:

//PushUp函数更新节点信息 ,这里是求和  
void PushUp(int rt){Sum[rt]=Sum[rt<<1]+Sum[rt<<1|1];}  
//Build函数建树   
void Build(int l,int r,int rt){ //l,r表示当前节点区间,rt表示当前节点编号  
    if(l==r) {//若到达叶节点   
        Sum[rt]=A[l];//储存数组值   
        return;  
    }  
    int m=(l+r)>>1;  
    //左右递归   
    Build(l,m,rt<<1);  
    Build(m+1,r,rt<<1|1);  
    //更新信息   
    PushUp(rt);  
}  

 

(2)点修改:

假设A[L]+=C:

void Update(int L,int C,int l,int r,int rt){//l,r表示当前节点区间,rt表示当前节点编号  
    if(l==r){//到叶节点,修改   
        Sum[rt]+=C;  
        return;  
    }  
    int m=(l+r)>>1;  
    //根据条件判断往左子树调用还是往右   
    if(L <= m) Update(L,C,l,m,rt<<1);  
    else       Update(L,C,m+1,r,rt<<1|1);  
    PushUp(rt);//子节点更新了,所以本节点也需要更新信息   
}   

 

(3)区间修改:

void Update(int L,int R,int C,int l,int r,int rt){//L,R表示操作区间,l,r表示当前节点区间,rt表示当前节点编号   
    if(L <= l && r <= R){//如果本区间完全在操作区间[L,R]以内   
        Sum[rt]+=C*(r-l+1);//更新数字和,向上保持正确  
        Add[rt]+=C;//增加Add标记,表示本区间的Sum正确,子区间的Sum仍需要根据Add的值来调整  
        return ;   
    }  
    int m=(l+r)>>1;  
    PushDown(rt,m-l+1,r-m);//下推标记  
    //这里判断左右子树跟[L,R]有无交集,有交集才递归   
    if(L <= m) Update(L,R,C,l,m,rt<<1);  
    if(R >  m) Update(L,R,C,m+1,r,rt<<1|1);   
    PushUp(rt);//更新本节点信息   
}   

 

(4)区间查询:

询问A[L,R]的和

首先是下推标记的函数:

void PushDown(int rt,int ln,int rn){  
    //ln,rn为左子树,右子树的数字数量。   
    if(Add[rt]){  
        //下推标记   
        Add[rt<<1]+=Add[rt];  
        Add[rt<<1|1]+=Add[rt];  
        //修改子节点的Sum使之与对应的Add相对应   
        Sum[rt<<1]+=Add[rt]*ln;  
        Sum[rt<<1|1]+=Add[rt]*rn;  
        //清除本节点标记   
        Add[rt]=0;  
    }  
}  


然后是区间查询的函数:

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)>>1;  
    //下推标记,否则Sum可能不正确  
    PushDown(rt,m-l+1,r-m);   
      
    //累计答案  
    int ANS=0;  
    if(L <= m) ANS+=Query(L,R,l,m,rt<<1);  
    if(R >  m) ANS+=Query(L,R,m+1,r,rt<<1|1);  
    return ANS;  
}   

 

(5)函数调用:

//建树   
Build(1,n,1);   
//点修改  
Update(L,C,1,n,1);  
//区间修改   
Update(L,R,C,1,n,1);  
//区间查询   
int ANS=Query(L,R,1,n,1);  

 

(2):最长连续零

题目:Codeforces 527C Glass Carving   题解

题意是给定一个矩形,不停地纵向或横向切割,问每次切割后,最大的矩形面积是多少。

最大矩形面积=最长的长*最宽的宽

这题,长宽都是10^5,所以,用01序列表示每个点是否被切割,然后,

最长的长就是长的最长连续0的数量+1

最长的宽就是宽的最长连续0的数量+1

于是用线段树维护最长连续零

 

问题转换成:

目标信息:区间最长连续零的个数

点信息:0 或 1

由于目标信息不符合区间加法,所以要扩充目标信息。

 

转换后的线段树结构

区间信息:从左,右开始的最长连续零,本区间是否全零,本区间最长连续零。

点信息:0 或 1

然后还是那2个问题:

 

1.区间加法:

这里,一个区间的最长连续零,需要考虑3部分:

-(1):左子区间最长连续零

-(2):右子区间最长连续零

-(3):左右子区间拼起来,而在中间生成的连续零(可能长于两个子区间的最长连续零)

而中间拼起来的部分长度,其实是左区间从右开始的最长连续零+右区间从左开始的最长连续零。

所以每个节点需要多两个量,来存从左右开始的最长连续零。

然而,左开始的最长连续零分两种情况,

--(1):左区间不是全零,那么等于左区间的左最长连续零

--(2):左区间全零,那么等于左区间0的个数加上右区间的左最长连续零

于是,需要知道左区间是否全零,于是再多加一个变量。

最终,通过维护4个值,达到了维护区间最长连续零的效果。

 

2.点信息->区间信息 : 

如果是0,那么  最长连续零=左最长连续零=右最长连续零=1 ,全零=true。

如果是1,那么  最长连续零=左最长连续零=右最长连续零=0, 全零=false。

 

至于修改和查询,有了区间加法之后,机械地写一下就好了。

由于这里其实只有对整个区间的查询,所以查询函数是不用写的,直接找根的统计信息就行了。

例题:

http://acm.hdu.edu.cn/showproblem.php?pid=1166 

http://acm.hdu.edu.cn/showproblem.php?pid=1698

http://acm.pku.edu.cn/JudgeOnline/problem?id=2777

http://acm.pku.edu.cn/JudgeOnline/problem?id=2528

http://acm.pku.edu.cn/JudgeOnline/problem?id=3667

http://acm.pku.edu.cn/JudgeOnline/problem?id=2828

http://acm.zcmu.edu.cn/JudgeOnline/problem.php?id=1948   

 题解:https://blog.csdn.net/qq_41713256/article/details/80482763

http://hdu.hustoj.com/showproblem.php?pid=4578(懒惰标记下推顺序)

题解:https://blog.csdn.net/qq_41713256/article/details/80496874

区间合并:http://hdu.hustoj.com/showproblem.php?pid=3308

题解:https://blog.csdn.net/qq_41713256/article/details/80508611

这些题目一定要做完,,,,,,,

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值