线段树详解

今天复习一下线段树。

1.基本结构

基本结构是这样的:

然后我们就可以建立一棵线段树。

根节点编号为1,之后第二层的第一个儿子编号为2,第二个儿子是3...

总结一句话,线段树是一颗二叉树,并且每个儿子节点都把父节点分成均匀的两个区间;若父节点的编号是x,那么两个子节点的编号就分别是2x2x+1

2.建树

建树怎么建呢?

从根节点开始,依次遍历它的两个儿子,然后如果发现l=r,那么此节点就是个叶子节点。

这时候,有人可能会问了,线段树可以干啥呢?

它的作用有很多,这里我举一个例子,它可以维护区间的最大值,并且支持修改操作。

那么我们就可以轻松建树。设每一个节点分别代表l~r这个区间的最大值,那么我们从根节点开始,如果发现左指针=右指针(它是个叶子节点),那么这个节点的最大值就是它的左指针在输入的数组中的值。

一个父节点的最大值就是它两个儿子之间的最大值。

void build(int l, int r, int v) { // 对 [l,r] 这个区间建立线段树,当前节点的编号为 p ,tree 为线段树,a 为输入数组
  if(l==r){
    tree[v]=a[l];
    return;
  }
  int mid=(l+r)/2;
  build(l,mid,v*2);// 递归对左区间建树
  build(mid+1,r,v*2+1);// 递归对右区间建树
  tree[v]=max(tree[v*2],tree[v*2+1]);//更新父节点
}

3.区间查询操作

还是刚刚的例子,如果我们要查询一个区间[l,r]间的最大值,如何用线段树维护呢?

首先,我们从根节点开始,我们设[s,t]为现在查找到的区间。然后我们发现,如果[s,t]在[l,r]里边(换句话说s ≥ l ,t ≤ r)。

我们发现如果当前的区间[s,t]的左区间与[l,r]有交集(就是包括,但不完全包括),那么我们就要查询左区间。

同理,如果它和右区间有交集,则查询右区间。

最终我们将查询到的结果返回即可。

void qry(int i,int s,int t,int l,int r){
	if(s<l||t>r) return ; //若这个区间和查询的区间没有交集,则直接返回
	if(l>=x&&r<=y){ans=max(ans,tree[i]); return ;} //如果直接包含了,那么直接取这个区间的最大值
	int mid=(l+r)/2;
	qry(i*2,l,mid);
	qry(i*2+1,mid+1,r);
}

4.区间/单点修改操作

<1>.单点修改

单点修改就很好修改了,上下跑一边就可以了。

具体的话就是从根节点向下遍历,直到找到这个节点的位置。然后再从这个节点一路向上修改,把它的父亲节点更新。

void add(int i,int l,int r){
	if(l==r){//如果是单点的话就直接修改
        tree[i]=y;
        return ;
    }
	int mid=(l+r)/2;
	if(x<=mid) add(i*2,l,mid);
	else add(i*2+1,mid+1,r);
	tree[i].maxn=max(tree[i*2].maxn,tree[i*2+1].maxn);//更新父节点
}

<2>.区间修改

区间修改则有点小复杂,它是把[l,r]这个区间都加上了一个数,然后再取最大值

这里我们需要引入一个叫做[懒惰标记](Lazy Tag)的东东

这个东东可以用来干什么呢?

首先,我们分析发现,如果对于[l,r]这个需要修改的区间,我们一个一个去修改的话,那么我们的时间复杂度就比暴力还多了log 2 n 倍,那么很明显,这个操作十分的不划算。

那么这个时候我们就要使用懒惰标记了!

何为懒惰标记呢?

看一张图片:

那么如图所示,我们就把[1,2]这个区间打上懒惰标记,并且把它的懒惰标记通过down操作传给它的儿子。

我们发现,其实对于一个打上了懒惰标记的点,我们只需要把它的两个儿子加上这个懒惰标记的值,然后再把它的两个儿子的懒惰标记加上它的懒惰标记就可以了。最后别忘了清空这个节点的懒惰标记

记住我们的区间查询操作也要改!

void down(int x){//lazy 为懒惰标记数组,tree 为线段树
	if(!lazy[x]) return ;
	lazy[x*2]+=lazy[x];
	lazy[x*2+1]+=lazy[x];//将它的懒惰标记传给它的儿子
	a[x*2].maxn+=lazy[x];
	a[x*2+1].maxn+=lazy[x];//让它的儿子也加上它的懒惰标记
	lazy[x]=0;//清空它的懒惰标记
}
void add(int i,int l,int r,int v,int x,int y){//x,y 为我们需要修改的区间,l,r为我们现在遍历到的区间,i为当前编号,v为我们要加的值
	if(x<=l&&y>=r){//如果需要修改的区间直接包括了当前遍历到的区间
		a[i].maxn+=v;
		lazy[i]+=v;//打上懒惰标记
		return ;
	}
	down(i);
	int mid=(l+r)/2;
	if(x<=mid) add(i*2,l,mid);
	if(y>mid)  add(i*2+1,mid+1,r);
	a[i].maxn=max(a[i*2].maxn,a[i*2+1].maxn); 
}
void qry(int i,int l,int r,int x,int y){//我们的查询操作也要变!!!!
	if(r<x||l>y) return ;//如果这俩区间完全不相交,直接返回
	if(l>=x&&r<=y){ans=max(ans,a[i].maxn); return ;}//如果直接包含,那么直接取max返回
	down(i);//记得这里也要down!!!
	int mid=(l+r)/2;
	if(x<=mid) qry(i*2,l,mid);
	if(y>mid) qry(i*2+1,mid+1,r);
}

The End

2022.5.2

posted @   RZC大蒟蒻  阅读(103)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示