线段树小结
对线段树基础的深入理解
线段树简介
线段树是二叉搜索树的一种,维护[1,N]区间中的值,左儿子维护[1,N>>1]的值,右儿子维护[N>>1|1,1]的值,不断递归,最后每个节点都能维护一段区间的值,非常擅长处理区间动态问题,每次操作的复杂度是O(logn)。就我学习的这段时间我认为大多数一维线性区间问题都可以利用线段树去做,而二维平面问题可以利用扫描线降维成一维来使用线段树,总之是非常强大的工具。
然而网上的大多数博客总是把有关线段树的一些骚操作全部分开来讲,找好的总结博客实在太累,这边小结一下方便自己以后回忆。
基础框架
基础框架是学的HH大神的线段树板子(密码:wmju),框架异常清晰,不愧是大神%%%,我自己习惯把树维护的东西全都封装在结构体里,这样敲起来虽然烦,但是看上去比较好理解(大概)。
线段树总体框架分为5个函数,下面介绍的几个骚操作都使用了下面至少两个框架:
struct TREE{
//线段树
}tree[N<<2];
// << 左移运算符,相当于*2^n操作
// >> 右移运算符,相当于/2^n操作
// | 或运算符
#define lson l,mid,rt<<1
#define rson mid+1,r,rt<<1|1
#define ls rt<<1
#define rs rt<<1|1
void pushup(){
//节点上传
}
void pushdown(){
//标记下传
}
void build(){
//建树
}
void update(){
//区间更新
}
void query(){
//区间询问
}
区间统计
给你一段区间,区间中的每个点都维护着一个值,问[L,R]区间中所有点的值的总和。
线段树的基础用法,维护不同区间中的和,思路也比较简单。
例题:找不到只考区间统计的题,下面会有题用到。
区间最值
一段区间内每个点都维护着一个值,问[L,R]区间中的最值是多少
线段树基础用法+1,不过是区间统计维护每个点的总和改为最值,思路简单+1
例题:HDU 1754: I Hate It(区间修改+区间最值)
题意:两种操作,第一种操作询问[A,B]区间的最高成绩,第二种是将A同学的成绩改成B。单点修改+区间最值板子题(单点修改在下面会介绍)。不过这题我利用树状数组去做了,目前没有线段树的实现(树状数组敲这题有点烦)
单点修改
一段区间内每个点都维护着一个值,但是会对单个点进行修改,然后在修改的过程中问你以上两种询问。
线段树基础用法+2,基础思路是update到最后一个点(目标点),然后在回溯的过程中不断pushup更新父节点。
例题:HDU 1166:敌兵布阵(单点修改+区间统计)
题意:初始给你敌军营地每个军营的敌人数量,有三种操作,第一二种表示第i个军营增加或者减少j个敌人,然后第三种操作是询问区间[i,j]敌人的数量。线段树单点修改区间访问板子题。
#include
#include
#include
#include
区间修改
一段区间内每个点都维护着一个值,但是会对一段区间的每个点进行修改,然后在修改的过程中问你以上两种询问。
线段树基础用法,基础思路是update到区间内的最后一个点(目标点),然后在回溯的过程中不断pushup更新父节点,但是对于一些数据量不是很大的题目中这么做及时单步操作不止 O(logn)也会超时,多数情况下就要用到后面会将的lazy-tag(懒惰标记)了。
例题:POJ 3468:A Simple Problem with Integers(区间修改+区间统计+懒惰标记)
题意:给定初始值,两种操作,一种是在区间[a,b]上增加c,另一种是询问区间[a,b]的和。区间修改+区间统计板子题。不过这题由于数字区间太大,直接开这么大的线段树妥妥炸,需要离散化
#include
#include
#include
#include
以上都是线段树的基础操作,接下来总结一下基础的骚操作
lazy-tag(懒惰标记)
对于一段区间的更新,多数情况下可能是不需要更新的那么详细的,因为询问操作很有可能到这一步就截然而止了,儿子的大小并不会被询问函数关注到,因此儿子的更新在这种情况下就并不显得那么重要,所以更新时完全没必要更新到儿子。
打个比方,比如我现在在[1,10]这段区间中需要更新,但是我在下一次询问的时候询问[1,5],很显然[1,3]和[4,5]以及它们的子区间并不会被询问到,因此我只需要更新到[1,5]区间就可以了。但是,加入你下下次的询问区间[1,3],这个时候如果不更新节点,那么这次的更新就会失效!因为你的更新在[1,5]就停止了!那么怎么办呢?我可不可以做一个标记,在我询问[1,3]区间的时候就可以利用这个标记继续更新[1,3]了!这个标记就叫lazy-tag,中文名懒惰标记。
但是懒惰标记对于初学者来说是学习线段树的第一道坎,对于每一种操作的标记不仅有不同的叠加方式,对于多种标记共同存在的情况顺序还不好处理,确实是要花一定时间思考的。
例题:HDU 4578:Transformation(区间修改+区间统计+懒惰标记)
题意:四种操作,第一种是将[x,y]区间替换成c,第二种是将[x,y]区间全部加c,第三种是将[x,y]区间全部乘c,第四种是询问[x,y]区间所有的值的p方和(1≤p≤3)。
题解:懒惰标记教你做人题,标记关系和维护方案都很难理解,码量也在惊人的200行,DeBug都能累死。
好在只要求1-3次方的和,那么我们就维护三棵线段树:区间一次方的和、二次方的和、三次方的和,更新时三棵树同时更新。
讲一下多种标记如何解决。第一种标记set标记比较简单粗暴,直接把add标记和mul标记清空(注意,加懒惰标记清为0,乘懒惰标记清为1),优先级最高。
接着是mul和add标记,比较难处理他们的关系,我们用一个例子解释
假设原来的值为x,它首先经历了乘法标记a,变成a*x。接下来加进来一个值b,变成了(ax+b),然后现在关键来了,我们乘进来一个值c,那么原本的乘法标记(mullazy == a)和加法标记(addlazy == b)要怎么处理呢?先观察它乘进来是啥:(ax+b)*c,根据分配率,它会变成a*cx+b*c,那么乘法标记不就是(mullazy == a*c),加法标记就是(addlazy == b*c),而这时再加一个d,(mullazy*x+addlazy)+d,也就addlazy+=d了,也就是说:当一个乘法标记a进来时,mullazy = a,addlazy = a;而一个加法标记b进来时,只要addlazy += b即可,而且从例子中也可以看出,对于值x,在处理时先乘上mullazy,在加上addlazy,即 X = (mullazy*x + addlazy)。
接下来就是p次方的和了,这需要用到这个:
(a+b)^2 = a^2+2ab+b^2
(a+b)^3 = a^3 + 3a^2b + 3ab^2 + c^3
我们虽然可以很快的得到答案,但是在加法懒惰标记的更新上,我们需要用到上面的公式。即
(sum1+a) = sum1+a
(sum2+a)^2 = sum2 + 2sum2*a + a^2
(sum3+a)^3 = sum3 + 3sum3^2*a + 3sum3*a^2 + a^3
具体操作还是看代码吧
#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include
#include
区间合并
给你一段区间,询问区间[l,r]中的最长连续区间,这时候就要用到区间合并了。
区间合并线段树的定义:
struct TREE{
int len;//这个节点维护的区间长度
int maxl,maxr,max;
//maxl:区间[l,r]中从l开始的区间的长度
//maxr:区间[l,r]中从r开始的区间的长度
//max:区间[l,r]中的最大长度
}tree[N<<2];
每个节点维护的值可以用下面三张图表示
(待补)
区间合并真正让人难以理解的是区间合并的pushup函数
#define ls rt<<1
#define rs rt<<1|1
void pushup(int rt){
tree[rt].max = max(tree[ls].maxr+tree[rs].maxl, max(tree[ls].maxl,tree[rs].maxr));
tree[rt].maxl = (tree[ls].maxl == tree[ls].len) ? tree[ls].maxl + tree[rs].maxl : tree[ls].maxl;
tree[rt].maxr = (tree[rs].maxr == tree[rs].len) ? tree[rs].maxr + tree[ls].maxr : tree[rs].maxr;
}
解释一下这三个值的维护方式:
> 1、对于tree[rt].max,按照上面三张图的解释就不难理解了,寻找左儿子的左连续区间、右儿子的右连续区间,左儿子右连续区间与右儿子右连续区间之和三者的最大值,就是这个点最大的连续区间。
>2、对于tree[rt].maxl,它虽然可以直接继承左儿子的左连续区间,但是有一个特殊情况,如果左儿子的左连续区间与他的长度相等,那么它有可能可以衔接上右儿子的左连续区间!
>3、对于tree[rt].maxr,同理2
很多人不理解2和3的原因,这里再用一张图解释
(待补)
衔接上右儿子的左连续区间后才是作为父亲的总的左连续区间,这就是区间合并
操作完pushup之后,tree[1].max就是要求的最大连续区间啦~
什么,你想知道[L,R]的最大连续区间?那就写query函数询问吧
int query(int L,int R,int l,int r,int rt){
if(tree[rt].max == 0 || tree[rt].max == r - l + 1) return tree[rt].max;
int mid = (l+r)>>1;
if(l <= mid)return r > mid - tree[rt].maxr ? tree[rt].maxr + tree[rp].maxl : query(L,R,lson);
else return r <= tree[rt].maxl + mid ? tree[tr].maxl + tree[tr].maxr : query(L,R,rson);
}
例题:HDU 4553:约会安排(区间合并)
题意:小明是个屌丝,帮他安排时间规划,题目有点绕,可以去上面的链接看。
题解:我觉得这题不能作为区间合并的板子题,然而集训的时候唯一的一题区间合并居然这么难。。开两棵树分别维护女神时间和屌丝时间,女神时间的优先级大于屌丝时间,给屌丝安排时间的时候只要问屌丝树有没有长度满足就可以了,而给女神安排时间需要先询问女神树,有满足长度的时间再去屌丝树上放屌丝的gezi
#include
#include
#include
#include
扫描线
在一个二维空间中,求几个随意放置的矩形面积交、面积并、周长并,可以利用扫描线的思想
很多博客解释扫描线是一条只存在脑内的不存在的线,确实扫描线是一个不存在的线,但是我认为把线段树维护的看做是那条线更容易理解扫描线的思想
对扫描线的深入理解
我们先定义扫描线为一棵线段树维护的线段,它维护的值是对应区间中矩形的数量。那么求解这一类问题,就要先搞定矩形数量与边的关系。假设扫描线从下往上扫,遇到矩形的边就停下来进行一次更新和计算,那么n个矩形就会遇到2*n条边(很显然),这些边我们把底部称为入边,扫描线经过这条边就说明边对应区间上的矩阵数量+1,就把线段树上相应区间标记+1,然后再把顶部称为出边,扫描线经过这里说明这条边上对应区间的矩阵数量-1,就把线段树上相应区间标记-1。这样就可以记录当前边上存在的矩阵数量了,当矩阵数量>=1就可以计算面积并,>=2就可以计算面积交,周长并就上下左右扫两次,遇到边的数量==1记录区间长度就好了。
例题:HDU 1255:覆盖的面积(离散化 + 扫描线)
题意:四个字,求面积交(扫描线板子题),由于涉及浮点数,而线段树不能处理浮点区间,需要离散化
#include
#include
#include
#include
小记
花了三天时间学习线段树(入门),觉得线段树是真的强大,但是代码量也不小,对于一些简单的任务分块和树状数组同样可以胜任相同的工作,但是你爹终究是你爸爸,想要实现更强大的算法少不了代码,也算是另一种以空间换时间的概念吧(还有敲代码的时间)。关于线段树还有主席树等更多的拓展,摸了摸自己日益稀疏的头,不知道还能撑多久。