浅谈线段树

news2024/9/21 4:29:10

1.前言

Oi-Wiki上的线段树

同步于 c n b l o g s cnblogs cnblogs发布。

如有错误,欢迎各位 dalao 们指出。

注:本篇文章个人见解较多,如有不适,请谅解。

前置芝士

1.二叉树的顺序储存

2.线段树是什么?

线段树,英文为 S e g m e n t Segment Segment t r e e tree tree。它是一种数据结构,主要解决区间修改和区间查询的问题。

我们用一个例题来引入线段树。

引例:

现在给定一个长度为 n n n n ≤ 1 0 6 n\le 10^6 n106)的序列 a a a。然后现在有 m m m m ≤ 1 0 6 m\le 10^6 m106)个询问操作,对于每一个操作,都有两种情况:

  • 1 x y,这一种操作表示将 a x a_x ax 加上 y y y

  • 2 l r,这一种操作表示查询 l − r l-r lr 这个区间的区间和,并输出。

对于这样的一个问题,不难发现,这是一个单点修改,区间查询的题目。

3.单点修改线段树

显然,对于刚才那道例题,我们运用暴力或者前缀和的思想在最坏情况下都会被卡到 O ( n m ) O(nm) O(nm),肯定是过不了这个题目的,如果要过这个题目,差不多也必须是在 O ( n l o g m ) O(nlogm) O(nlogm) 或者 O ( m l o g n ) O(mlogn) O(mlogn) 时间复杂度以内的算法才能够承受。因此,我们的线段树算法就此横空出世!

其实我们在学树状数组的时候,我们有一种思想就是我们通过二进制分解将一个区间化为 l o g n logn logn 个子区间。那

我们可以将 a a a 序列的 1 1 1 n n n 这个区间化成左右两个区间。为了方便举例,我们假定现在 n = 5 n=5 n=5。显然,我们可以把 [ 1 , n ] [1,n] [1,n] 化为 区间 [ 1 , 2 ] [1,2] [1,2] 和区间 [ 3 , 5 ] [3,5] [3,5]。现在定义一个区间的左右两个区间分别为这个区间的左孩子和右孩子。例如现在的区间为 [ x , y ] [x,y] [x,y],则他的左孩子就是 [ x , ( x + y ) / 2 ] [x,(x+y)/2] [x,(x+y)/2],右孩子就是 [ ( x + y ) / 2 + 1 , y ] [(x+y)/2+1,y] [(x+y)/2+1,y]。特别要注意的是如果 x = y x=y x=y,则他就是叶子节点,因为这个区间已经不能再分了。

则我们现在就可以把一个 n = 5 n=5 n=5 的序列化为一棵树。

qwq

如图,我们可以发现,所有叶子节点的区间左端点和右端点都是相等的。并且由于我们需要的是区间和,所以我们需要把书上每个区间的和的统计出来。我们可以根节点 [ 1 , 5 ] [1,5] [1,5] 这个区间编号为 1 1 1,则通过二叉树的顺序储存原理,编号为 x x x 的节点,他的左儿子为 2 x 2x 2x,右儿子为 2 x + 1 2x+1 2x+1 。为了方便查询一个区间的左儿子和右儿子,我们就运用这种顺序储存原理来储存这课树。

我们可以运用递归来进行建树。

建树参考代码如下:

struct segmentree
{
	int l,r;//当前节点的左端点和右端点
	long long data;//当前这个区间的区间和
}tree[maxe];//顺序储存,maxe 表示 4*n。(仔细想一想为什么要开4倍)
void build(int p,int l,int r)//p表示节点,l,r表示当前节点的做右端点
{
	if(l==r)//如果左右端点相等,就表明这个节点是叶子节点,并用继续递归下去了。
	{
   	  //将当前节点的三个信息储存下来。
		tree[p].l=l;
		tree[p].r=r;
		tree[p].data=a[l];
		return;
	}
	int mid=(l+r)>>1;
    //对他的左右儿子进行建树
	build(p*2,l,mid);
	build(p*2+1,mid+1,r);
	tree[p].l=l,tree[p].r=r;
	tree[p].data=tree[p*2].data+tree[p*2+1].data;//由于当前区间有左右孩子,所以当前区间的区间和就等于他的左右孩子的区间和之和。
}

建完树之后,接下来的问题就是修改操作。

我们可以发现,如果修改了第 x x x 位上的值,则所有包含了 x x x 的区间的区间和都要改变,也就是对一个 n = 5 , x = 3 n=5,x=3 n=5,x=3 的树, a x a_x ax 被修改了之后,我们需要将途中所有标橙了的区间的区间和修改掉。

qwq

可以发现,你无论修改的是哪一位上的数值,要被改动的区间和最坏是 ( l o g n + 1 ) (logn+1) (logn+1) (向上取整)次,所以时间复杂度为 m l o g n mlogn mlogn

对于修改操作的代码实现,仍然使用递归。

修改操作参考代码:

void change(int p,int x,int y)//p表示当前递归到的节点编号,x,y表示将a[x]改为y
{
	if(tree[p].l>x||tree[p].r<x)//如果当前这个节点的区间不包含x,则不需要在递归下去。
	return;
   	if(tree[p].l==x&&tree[p].r==x)
   	{
   		tree[p].data+=y;
   		return;
	}
  	//对他的孩子进行递归。
	change(p*2,x,y);
	change(p*2+1,x,y);
	tree[p].data=tree[p*2].data+tree[p*2+1].data;//当前区间的区间和就是他的左右儿子之和 
}

最后就是查询区间和操作。

比较容易发现,我们如果要查询 [ l , r ] [l,r] [l,r] 区间的区间和,这个区间可以在我们拆出来的二叉树中找到 l o g n logn logn 个极大区间,而我们只需要把所有拆分出来的这些区间的和给统计起来即可。时间复杂度仍然为 O ( m l o g n ) O(mlogn) O(mlogn)

区间查询参考代码:

long long ask(int p,int l,int r)//p表示当前节点编号,l,r表示要查询的[l,r]区间
{
	if(tree[p].r<l||tree[p].l>r)//如果当前枚举到的区间与查询的区间没有交集,则就没有必要在查询下去了
	return 0;
	if(tree[p].r<=r&&tree[p].l>=l)//如果要查询的区间包含现在美剧道德区间,则我们就直接返回这个区间的区间和
	{
		return tree[p].data;
	}
	long long res=0;
	res+=ask(p*2,l,r)+ask(p*2+1,l,r);//继续递归下去的时候,要把它的左右孩子递归得到的值给储存下来。
	return res;
}

4.线段树求区间最大最小值

上面我们讲的是线段树快速求出一个区间 [ l , r ] [l,r] [l,r] 的区间和,现在我们要讨论的就是如何运用线段树求出 [ l , r ] [l,r] [l,r] 求出一个区间的最大最小值。

我们现在直接以求最大值进行举例。

对于建树,由于是求最大值,所以我们应该将建树的倒数第二行改成求两个左右儿子节点的最大值。

建树参考代码如下:

void build(int p,int l,int r)
{
	if(l==r)
	{
		tree[p].l=l;
		tree[p].r=r;
		tree[p].data=a[l];
		return;
	}
	int mid=(l+r)>>1;
	build(p*2,l,mid);
	build(p*2+1,mid+1,r);
	tree[p].l=l,tree[p].r=r;
	tree[p].data=max(tree[p*2].data,tree[p*2+1].data);//与区间和的唯一区别就在这里,是求最大值而不是求和
}

对于单点修改,我们同样需要修改倒数第二行,因为它是取最大值。特别要注意的是,如果当前递归道的区间就是 [ x , x ] [x,x] [x,x],则我们需要直接将这个区间的最大值设为 y y y,因为他的修改操作是 a x = y a_x=y ax=y

修改操作参考代码如下:

void change(int p,int x,int y)
{
	if(tree[p].l>x||tree[p].r<x)
	return;
	if(tree[p].l==x&&tree[p].r==x)
	{
		tree[p].data=y;//直接设为y
		return;
	}
	change(p*2,x,y);
	change(p*2+1,x,y);
	tree[p].data=max(tree[p*2].data,tree[p*2+1].data);//这里是区最大值
}

最后是查询操作,这个也一样,因为是求最大值,所以我们仍然只需要改成 m a x max max 即可。

查询操作参考代码如下:

int ask(int p,int l,int r)
{
	if(tree[p].r<l||tree[p].l>r)
	return INT_MIN;
	if(tree[p].r<=r&&tree[p].l>=l)
	{
		return tree[p].data;
	}
	int res=0;
	res=max(ask(p*2,l,r),ask(p*2+1,l,r));//注意是最大值
	return res;
}

5.线段树的区间修改以及懒惰标记

上文中,我们讲到了线段树可以维护单点修改,区间查询最大值和区间和。这时,你可能会问,那是不是线段树就不支持区间修改了呢?

Too young to simple! 线段树这么有用的数据结构怎么可能维护不了区间修改这种东西。那么接下来,就给大家介绍如何运用线段树解决区间修改的问题,

先抛出一个例题,洛谷 P3372 【模板】线段树 1。

读完题目后,你发现,这不就是线段树区间修改和区间查询的模板题吗?

对于这个题目,要用线段树做的话我们首先是建树。对于这一个建树的话没有什么可以说的,跟线段树的普通建树一样,这里不做过多阐述。

现在最头疼的就是这个区间修改操作,我们知道,如果要修改整个区间且运用普通线段树,则我们需要将所有与这个区间有交集的区间的区间和都要修改,速度最慢可以卡到 O ( n ) O(n) O(n),还没有普通的暴力快,那我们该怎么办呢?

这个时候,我们要引入一个叫做懒惰标记的东西。

我们可以对于树上的每一个节点再定义一个东西,叫做 l a z y t a g lazytag lazytag。我们运用它来储存这整个区间整体每个数被加了多少。例如我们现在将 [ 1 , 5 ] [1,5] [1,5] 这个区间全部加上 3 3 3 。则对于区间 [ 2 , 4 ] [2,4] [2,4] 来说,它的 l a z y t a g lazytag lazytag 就等于 3 3 3

当我们在递归修改时,如果当前要修改的这个区间包含了现在遍历到的节点的区间,则我们就将它的 l a z y t a g + lazytag+ lazytag+要区间加的值,并将整个区间的区间和修改。如果当前遍历到的区间只与要修改的区间存在交集,那对于普通的线段树,我们就要对他的左右儿子进行遍历。但是由于当前枚举到的这个区间的懒惰标记只记录了这个区间的,而我们还没有对它进行下传,且马上又要遍历他的子节点。所以这个时候我们就需要将这个懒惰标记下传到它的子节点身上,并修改相应的值,再进行对它儿子的递归。

修改操作参考代码如下:

void pushdown(int p)//这是懒惰标记下传操作
{
	if(tree[p].lazytag)//如果当前节点有懒惰标记需要下传
	{
		int l=p*2,r=p*2+1;//l,r分别表示当前节点的左右儿子
		long long tag=tree[p].lazytag;//tag来记录当前节点的懒惰标记
		tree[l].data+=(long long)(tree[l].r-tree[l].l+1)*tag;//下传的途中,我们需要将两个区间的和都进行修改,也就是加上区间长度*懒惰标记。
		tree[r].data+=(long long)(tree[r].r-tree[r].l+1)*tag;//同上
		tree[l].lazytag+=tag;//对懒惰标记进行下传
		tree[r].lazytag+=tag;//同上
		tree[p].lazytag=0;//下传完之后要将当前节点的懒惰标记清零,因为已经下传
	}
}
void change(int p,int l,int r,long long x)
{
	if(tree[p].l>r||tree[p].r<l)//如果不存在交集,就不需要再进行递归了
	return;
	if(tree[p].l>=l&&tree[p].r<=r)
	{
		tree[p].data+=(long long)(tree[p].r-tree[p].l+1)*x;//区间长度呈上要修改的值
		tree[p].lazytag+=x;//修改懒惰标记
		return;
	}
	pushdown(p);//标记下传
   	//普通线段树的递归操作
	change(p*2,l,r,x);
	change(p*2+1,l,r,x);
	tree[p].data=tree[p*2].data+tree[p*2+1].data; 
}

最后就是区间查询,在区间查询的时候,我们如果当前要查询的区间和枚举到的节点的区间存在交集,则我们同样需要更新他左右儿子的值并下传懒惰标记,其余的都不需要改变。

区间查询参考代码:

long long ask(int p,int l,int r)
{
	if(tree[p].r<l||tree[p].l>r)
	return 0;
	if(tree[p].r<=r&&tree[p].l>=l)
	{
		return tree[p].data;
	}
	pushdown(p);//与普通线段树的唯一区别就是这里要下传懒惰标记和修改左右子节点的区间和,因为后面需要查询左右儿子的信息。
	long long res=0;
	res=ask(p*2,l,r)+ask(p*2+1,l,r);
	return res;
}

6.后记

今天对于线段树就谈到这里了,如果大家想练习线段树的话,我下面抛给大家几个例题。

P3373【模板】线段树 2

P7764 [COCI2016-2017#5] Poklon

P1168 中位数

See you next time~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/729537.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

“量贩零食”热潮袭来:真风口还是假繁荣?

以前只听过量贩式KTV&#xff0c;现在“量贩零食店”也出现在了大街小巷。 高考结束后&#xff0c;家住武汉的花花频繁逛起了量贩零食店。这类店把各种零食集合在一起销售&#xff0c;用低价来换取高销量&#xff0c;主打一个性价比。店里的散装零食即便按斤售卖&#xff0c;也…

蛋白组学 差异蛋白分析 富集分析 go kegg

生信学习day1-蛋白组分析 蛋白质组差异分析的三个R包 - 知乎 (zhihu.com)https://zhuanlan.zhihu.com/p/448479536Introduction to DEP (bioconductor.org)http://www.bioconductor.org/packages/release/bioc/vignettes/DEP/inst/doc/DEP.html#lfq-based-dep-analysis浅谈蛋白…

阿里云coluder认证训练营开班!

在这个充满机遇和挑战的时代里&#xff0c;云计算已经成为推动企业创新和发展的关键技术。而作为云计算人才培训领域的领军企业&#xff0c;摩尔狮致力于培养更多优秀的云技术人才&#xff0c; 所以摩尔狮联合阿里云为大家打造了免费的云计算入门课程——Clouder认证集训营&…

全方位了解VR全景展示与制作

引言&#xff1a; 虚拟现实&#xff08;VR&#xff09;技术正在以惊人的速度改变我们的生活方式和体验方式。其中&#xff0c;VR全景展示与制作作为虚拟现实的重要应用之一&#xff0c;为用户提供了身临其境的视听体验。 一、了解VR全景展示与制作 1.VR全景展示 VR全景展示是…

JDK,JRE,JVM的区别

1.JVM JVM&#xff0c;也叫java虚拟机&#xff0c;用来运行字节码文件&#xff0c;可将字节码翻译为机器码&#xff0c;JVM是实现java跨平台的关键&#xff0c;可以让相同的java代码在不同的操作系统上运行出相同的结果。 2.JRE JRE&#xff0c;也叫java运行时环境&#xff…

【JS】javascript学习笔记

step by step. 目录 严格区分大小写 点击事件&#xff1a; JavaScript关键字/语句标识符 数据类型 对象Object 创建对象方法 事件 循环 标签 正则表达式 异常 未定义adddlert-> throw—— 调试工具debugger ​编辑 JS严格模式 表单 严格区分大小写 点击事件&am…

python3 学习笔记

一、注释 1.单行注释&#xff1a;# 开头 2.多行注释&#xff1a; 和 """ 二、缩进 python是使用缩进来表示代码块&#xff0c;不需要使用大括号{} python具有严格的缩进原则&#xff0c;每个缩进一般可以有两个或四个空格组成&#xff0c;也可以是任意数量的…

深度学习常用优化器总结

一、优化器的定义 优化器&#xff08;optimizer&#xff09;本质上是一种算法&#xff0c;用于优化深度学习模型的参数&#xff0c;通过不断更新模型的参数来最小化模型损失。在选择优化器时&#xff0c;需要考虑模型的结构、模型的数据量、模型的目标函数等因素。 二、常用…

web前端总结(一)HTML标签

1.语法结构&#xff1a; <标签 属性 “值”>内容</标签> <p align "center">标签内容</P> 2.标签 1.标题标签&#xff1a; **标题标签 <h1> - <h6>&#xff08;重要&#xff09;** 为了使网页更具有语义化&#xff0c;我们…

stm32_<一文通>_cubemx_freertos

文章目录 前言一、任务调度1.1 延时1.1.1 相对延时1.1.2 绝对延时 1.2 挂起和恢复1.2.1 cmsis的挂起和恢复函数1.2.2 freertos的挂起和恢复函数 1.3 删除1.3.1 cmsis的删除任务函数1.3.2 freertos的删除任务函数 二、Freertos任务与中断三、消息队列3.1 写入和读取一个数据3.2 …

6阶高清视频滤波驱动MS1681

MS1681 是一个单通道视频缓冲器&#xff0c;它内部集成6dB 增益的轨到轨输出驱动器和6 阶输出重建滤波器。MS1681 的-3dB 带宽为35MHz&#xff0c;压摆率为160V/us。MS1681 比无源LC 滤波器与外加驱动的解决方案能提供更好的图像质量。它单电源供电范围为2.5V 到5.5V&#xff0…

什么是提示词工程师?

前言 你可能听说过人工智能模型&#xff0c;但你是否知道&#xff0c;背后的神奇之处源自于那些执着于提示设计和优化的专业人员&#xff1f;提示词工程师是引导我们与机器对话的幕后英雄&#xff0c;他们通过精心构造的提示&#xff0c;让模型理解我们的意图、解答问题&#…

React + TypeScript 实践

主要内容包括准备知识、如何引入 React、函数式组件的声明方式、Hooks、useRef<T>、useEffect、useMemo<T> / useCallback<T>、自定义 Hooks、默认属性 defaultProps、Types or Interfaces、获取未导出的 Type、Props、常用 Props ts 类型、常用 React 属性类…

zabbix基础4——自定义监控案例

文章目录 一、监控进程二、监控日志三、监控mysql主从四、监控mysql延迟 一、监控进程 示例&#xff1a;监控客户端上的httpd服务进程&#xff0c;当进程书少于1时&#xff0c;说明服务已经挂掉&#xff0c;需要及时处理。 1.客户端开启自定义监控功能。 vim /usr/local/etc/…

YApi-高效、易用、功能强大的可视化接口管理平台——(一)使用 Docker 本地部署

Docker 本地部署 YApi 安装 Docker安装设置 USTC 镜像启动 Docker Docker 安装 MongoDBDocker 安装 YApi登录 YApi 本内容以虚拟机【系统&#xff1a;Centos7】为例&#xff0c;云服务器步骤相同。使用Docker 的方式搭建 YApi&#xff0c;拉取 MongoDB 镜像和 YApi 镜像即可。 …

SpringBoot学习——追根溯源servlet是啥,tomcat是啥,maven是啥 springBoot项目初步,maven构建,打包 测试

目录 引出追根溯源&#xff0c;过渡衔接servlet是啥&#xff1f;tomcat是啥&#xff1f; 前后端开发的模式1.开发模式&#xff1a;JavaWeb&#xff1a;MVC模型2.Web&#xff1a;Vue&#xff0c;MVVC模型3.后端相关3.1 同步与异步3.2 Controller层3.3 Service层&#xff1a;要加…

【C语言基础】函数(2)

在函数&#xff08;1&#xff09;中我们已经讲过了函数的定义&#xff0c;形参与实参&#xff0c;函数的调用&#xff0c;局部变量与栈内存 接下来还有几个要强调的函数相关知识。 一、静态函数 静态函数是在函数声明前加上关键字 static 的函数。静态函数在C语言中具有以…

让计算机讲话

std ::cout是什么&#xff1f; 首先 std是命名空间后面会讲。STD&#xff08;standard标准&#xff09; cout和printf就呢让计算机说话 最早的使用打印机&#xff0c; std::cout相当于调用了一个特殊的函数。 后面需要加<<隔开。 知道最基本的知识就OK了 c的风格 函数…

API接口在软件开发中扮演着重要的角色

随着互联网的快速发展&#xff0c;API&#xff08;Application Programming Interface&#xff09;接口在软件开发中扮演着重要的角色。调试API接口是确保系统正常运行的关键步骤之一。本文将介绍如何选择适合的方法进行API接口调试&#xff0c;以确保开发过程的高效进行和应用…

20.光敏传感器

1.光敏传感器介绍&#xff1a; 光敏二极管(光敏电阻),作为光敏传感器&#xff1b;光敏二极管也称光电二极管&#xff1b;光敏二极管与半导体二极管在结构上类似&#xff0c;其管芯是一个具有光敏特征的PN结&#xff0c;具有单向导电性&#xff0c;因此工作时需要加上反向电压。…