本篇对应的是luogu的线段树1
概况:
如下图就是一棵线段树,线段树上的每一个点记录的都是一个区间,所以线段树支持对于区间和点的动态操作,可以在线查询和更改区间上的最值,求和等
时间复杂度:O(n)
使用线段树的情况:
满足区间加法:已知左右两子树的全部信息,一定能够推出父节点
线段树维护的内容根据题目的要求而定
线段树的分类:
根据题目中对于查询和修改区间的不同要求,大致将线段树分为三类:
problem1:单点修改,单点查询
problem2:区间修改,单点查询
problem3:区间修改,区间查询
思路:
思路+代码阅读(注:不同题目线段树维护的内容不同,这里给出区间和写法,如果要维护其他内容,只需把下面所有提及sum的内容更改为新的转移方程即可):
001:建立线段树 利用二分,不断维护最终要求的值,返回条件为找到叶节点O(n)
//构造一棵a1-an的线段树 调用build_xdtree(1,1,a)
void build_xdtree(int o,int l,int r){
//o是当前这段区间的点编号, sum[o]用于记录区间和,l是左节点,r是右节点
if(r==l){ //如果已经找到叶节点,它的区间和就是它自己的值
sum[o]=a[l]; //a[l]用于记录每个点本身的值
return; //找到叶节点,返回
}
int mid=(l+r)>>1; //从中间二分
build_xdtree(o<<1,l,mi); //分别build它的左右节点
build_xdtree(o<<1+1,mid+1,r);
sum[o]=sum[o<<1]+sum[o<<1+1]; //它的区间和即左右区间和的和
}
002:PROB1 单点修改和单点查询
单点修改:O(log2(n))
从根节点开始找,判断x在左右子树
最后更新被影响到的值(和建树相同),条件为叶节点(x)
//线段树点的更新 如果更新a[x]=y
void uponedate(into,intl,intr,intx,int y){
if(l==r){ //找到根节点,也就是x
sum[o]=y;//更新根节点为新的y
return; //返回
}
int mid=(l+r)>>2;
if(x<=mid){ //如果x在o的左子树里
update(o<<2,l,mid,x,y); //在左子树里继续找,右子树的值不受影响
}
else update(O<<2+1,mid+1,r,x,y);
//反之在右节点里更新 左节点的值不受影响
sum[o]=sum[o<<2]+sum[o<<2+1];
//更新sum[o]的值为更新过后的左右子树之和
}
单点查询:O(log2(n))
从根节点开始找,终止条件为覆盖区间,分别判断左右子树有没有包含区间一部分
//在只修改点情况下线段树的查询区间ax-ay ans用于记录结果
void onequery(int o,int l,int r,int x,int y){
if(x<=l&&y>=r){//思考:为什么这样写
ans+=sum[o];//对于覆盖区间[l,r]的[x,y],ans直接加上[l,r]的sum
return;//实际上就等同于找到了完全相等的区间,可以不用向下搜了
}
int mid=(l+r)>>1;
if(x<=mid){ //如果有一部分在[l,r]的左子树里,就在左子树里搜索
query(o<<1,l,mid,x,y)
}
if(y>mid){ //如果有一部分在[l,r]的右子树里,就在右子树里搜索
query(O<<1+1,mid+1,r,x,y);
}
}
003:lazy_tag(重点)
对于区间的修改,可以定义一个lazytag,给这个区间打上标记来代替直接对它进行操作,那么在读取或进行其他操作的时候就可以一起读出lazytag,优化了时间复杂度,注意:假设a[o]有一个大小为k的tag,意味着它的子节点都欠着一个tagk没有加。但是a[o]不欠,它的值在调用更改函数时(或者由它的父亲传下来时),就已经更新过了。所以对于a[o]而言,决定它大小的是它的父亲的tag。
push up: 读完tag之后,所有的儿子会更新它的数值(可能是最值、区间和等),那么他们的父亲也会随之更新数值,所以在所有的递归改变了儿子之后,都要调用pushup来更新父亲
// 如果一个子节点加了x,那么它的父节点也要加上x 在每次递归完左右儿子后调用
void pushup(int o){
sum[o]=sum[o<<2]+sum[o<<2+1];
}
push down:父亲节点的tag会对子节点的数值造成影响,所以在所有的递归儿子之前,为了准确计算儿子的值,都要把父亲的tag传递给它的子节点,并更新子节点的值,同时可以清除父亲的tag,因为他的子节点更新了
// 将父节点的tag进行下传 在每次递归左右儿子前调用
void pushdown(int o,int l,int,r,int mid){
if(tag[o]){//如果父节点的tag不为0
tag[o<<1+1]+=tag[o];//子节点可能会多次更新tag
tag[o<<1]+=tag[o];//下传 为了不影响tag的添加
sum[o<<1]+=tag[o]*(mid-l+1);//节点长度
sum[o<<1+1]+=tag[o]*(r-mid+1); //更新sum的值
tag[o]=0;//父节点的tag都下传了 所以更新为0
}
}
004:PROB2 区间修改和区间查询
区间修改:
从根节点开始,找到覆盖就打tag,更新值,返回
下传tag,分别判断左右子树有没有包含区间一部分来递归,更新父节点
//线段树的区间修改 修改内容为ax—ay都加v
void update(int o,int l,int r,int x,int y,int v){//当前搜索到的点
if(x<=l&&y>=r){//如果[x,y]覆盖了当前找到的[l,r]
tag[o]+=v;//给区间[l,r]打上tag,那么就不用再找他的孩子
sum[o]+=(r-l+1)*tag[o];//前面说过,此时要更新sum[o]的值
return ;//不用找它的孩子,直接返回
}
else{ //开始遍历左右子树
pushdown(o,l,r,(l+r)>>1);
//遍历前先下传tag,如果不传,它的子节点就还欠债,计算的值就有错误
int mid=(l+r)>>1;
if(x<=mid){ //如果左子树里有[x,y]的一部分
update(o<<1,l,mid,x,y,v);
}
if(y>mid){ //如果右子树里有[x,y]的一部分
update(o<<1+1,mid+1,r,x,y,v)
}
pushup(o);//在搜完左右节点以后,更新一下当前节点的信息
}
}
区间查询:
//线段树的查询区间ax-ay 直接返回
int query(int o,int l,int r,int x,int y){
if(x<=l&&y>=r){//如果[x,y]覆盖了[l,r]区间,[l,r]区间的值就是答案的一部分
return sum[o];//因为是int型,直接返回[l,r]的值,不需要向下搜了
}
else{
pushdown(o,l,r,(l+r)/2);//涉及到子节点的值,先打tag
int mid=(l+r)>>1;
int ans=0; //记录答案
if(x<=mid){ //如果有一部分在[l,r]的左子树里
ans+=query(o<<1,l,mid,x,y)
}
if(y>mid){ //如果有一部分在[l,r]的右子树里
ans+=query(o<<1+1,mid+1,r,x,y);
}
return ans; //最后不需要更新父节点,因为没有改变子节点的大小
}
}
005 整理
思考:
001 线段树的结构,可以解决的问题类型,时间复杂度
002 如何建立一棵线段树呢
003 在写所有关于点的函数时,他们有什么共同点和不同点
比如:定义内容,终止条件,更新内容,分类标准,更不更新父节点
004 在写所有关于区间的函数时,他们有什么共同点和不同点
比如:定义内容,终止条件,更新内容,分类标准,下传tag,更不更新父节点
回忆上面内容中标红(细节)和下划线的地方(结构),尝试还原算法
---END---
第一版:2019-10-02