线段树详解

news2025/1/9 15:32:09

目录

线段树的概念

线段树的实现

线段树的存储

需要4n大小的数组

线段树的区间是确定的

线段树的难点在于lazy操作

代码样例


线段树的概念

线段树(Segment Tree)是一种平衡二叉树,用于解决区间查询问题。它将一个区间划分成若干个子区间,从而实现对整个区间的快速查询和修改。线段树可以表示固定区间上的某种信息,例如区间和、区间最大值、区间最小值等。每个非叶子节点维护其子节点所包含区间的信息,所以,线段树可以高效地处理区间查询和区间修改等操作。

线段树可以应用于多种场景,比如对于静态问题可以使用单次建树的线段树,对于动态问题可以使用可持久化线段树,在此基础上实现区间修改和查询的历史版本记录。线段树还可以结合线段树的优化技巧来达到更高的效率,例如使用线性预处理、差分、前缀和等技巧降低查询和修改的时间复杂度,利用分支预测和循环展开降低分支跳转的代价等。

线段树的实现

线段树的每一个节点都表示一个区间,设区间被mid分割,则mid左边的范围表示节点左子树的区间,右边的范围表示节点右子树的区间。具体图例如下。

线段树的存储

        通过上图不难发现,线段树是一种完全二叉树。因为我们对线段树的区间进行拆分的时候,是一个类似二分的折半拆分,将节点的区间均分成两段,分别对应其左右孩子,直至节点的左右区间相等。

        所以在实现线段树时,我们一般选择用数组实现,并舍弃数组下标的0位置以1开头。这是因为如果根节点序号为1,那么对于任意一个节点序号n,那么其对应左子树的序号就为2n,右子树的序号为2n+1。我们在线段树的操作中一般就是通过数组2n和2n+1的数组下标访问节点的左右孩子的。

        值得注意的是,线段树的每一个节点都表示的是一个区间范围,叶子节点也不例外,只不过叶子节点的左右区间相等,所以可以看作就是一个点。所以通常线段树的结构中都会有一个left和一个right用来表示维护的区间范围。

需要4n大小的数组

        对于一个区间范围为1到n的线段树,我们一般将线段树的数组大小设置为4n。

        这是因为线段树中的每个节点都表示一个区间。如果线段树是一棵完全二叉树,那么它的节点数会恰好是2n-1个(区间范围是1-n,表示最终的叶子节点有n个,那么总结点数就是2n-1)。但如果不是完全二叉树,那么总节点数目就是大于n小于等于2n-1的。又因为数组是一种线性连续的存储结构(即下标是连续的),所以为了通过2n+1这种方式来索引,就需要再为每一个叶子节点分别添加左右孩子节点,即还需再添加2n个节点(n个叶子节点,每个节点添加两个孩子节点,共添加2n个节点)。

        所以最后需要的总节点数为:完全二叉树的情况需要2n-1个;非完全二叉树的情况需要大于3n小于等于4n-1个。所以为了保险起见我们取一个最安全的4n为数组大小。

线段树的区间是确定的

        线段树的区间在建树时是确定的,一旦建立就不会改变。但是线段树中节点对应的区间可以随着查询或者修改操作的需要而动态变化。

        所以通常情况下,线段树不支持删除操作。因为删除一个元素需要更新该元素所属的所有区间的信息,这个过程会比较复杂,而且还需要维护大量的信息。如果必须要删除某个元素,一种简单的思路是将其标记为“删除”,然后在查询时忽略这些“删除”的元素。

        更普遍的情况是,对于线段树中的每个节点,我们都会记录一个标志位,表示这个节点所代表的区间是否有效或已删除。这个标志位可以放在线段树节点中对应的元素上,或者另外维护一个与线段树节点对应的标志数组。在查询或更新时,我们可以根据区间有效性进行判断,忽略已删除的区间。

线段树的难点在于lazy操作

        设大L、R为为操作区间,小l、r为线段树的当前节点区间。如果要对[L,R]范围内的的数进行操作,首先判需要断当前节点所代表的区间与待修改区间之间的关系。如果这两个区间没有重叠部分,那么不需要对该节点进行修改操作,直接退出即可。如果存在重叠部分,则需要递归地访问该节点的左右子树,并将修改操作应用于相应的节点。在递归返回过程中,需要将子树的修改结果合并到当前节点。

        我们以add操作为例,假设要将区间为1-n的线段树中[L,R]范围的数同时加上一个val,那么操作的大致思路是:

      如果[L,R]完全包含当前节点的[l,r],那么就直接将当前节点的lazy值设为val,同时将当前节点的值加上val*(r-l+1) —— r-l+1为当前节点的孩子数量。

      如果[L,R]包含但并不是全包含[l,r],那么就进行如下操作:

              1、下放当前节点lazy值到左右孩子(pushdown操作)

              2、判断[L,R]区间与左右孩子区间的关系,决定是否有必要进入左孩子或者右孩子。

      如果 L <= mid,说明[L,R]区间与左孩子的区间有交集,那么进入左孩子;同理,如果R >= mid+1,那么就进入右孩子。

      如果[L,R]与[l,r]区间并无交集,那么就什么都不需要做。

      最后,进行节点汇总(pushup操作)


而对于修改(change)等操作也是换汤不换药,大体过程类似,只不过修改操作是将所有add操作的lazy值清空,change操作的lazy值改为要修改的值(这是两个lazy值,实现时是不同的变量名)。具体的实现可以看下面的代码部分。

但需要注意的是对于pushdown方法的书写要先下放change的lazy值再下放add的lazy值。这是因为change操作会抹去add的lazy值,所以如果change的lazy值和add的lazy值同时存在,那一定是先进行了change操作之后又进行的add操作。

代码样例

#include<stdio.h> 
#include<stdlib.h>
#include<string.h>
#include<stdbool.h>
#include<assert.h>


#define MAXN 9 //注意,0位置弃掉不用,所以默认数组的有效区间为[1,MAXN-1]
//线段树结构
typedef struct
{
	struct
	{
		int l, r;		//l-r表示当前节点代表的区间 
		int sum;		//存放区间l-r范围内元素的和 
		int lazy;		//存放区间l-r范围内的lazy值 
		int change;		//存放区间l-r范围内的更新值 
		bool updata;	//存放区间l-r范围updata标志 
	//开辟数组的大小为4n
	}SNode[MAXN << 2]; 	
}SegTree; 
//更新tree树中下标为rt的节点的sum
void PushUp(SegTree* tree, int rt) 
{
	//rt位置的sum更新为 2*rt(左孩子) 与 2*rt+1(右孩子) 位置的sum之和
	(*tree).SNode[rt].sum = (*tree).SNode[rt << 1].sum + (*tree).SNode[rt << 1 | 1].sum;
	/* PS:<< 比 * 运算要快一些 */ 
}
//tree树中的rt节点的lazy/change下放
void PushDown(SegTree* tree, int rt)
{
	int l = (*tree).SNode[rt].l, r = (*tree).SNode[rt].r, mid = l + (r - l) / 2;
	//change下放
	bool changeTag = (*tree).SNode[rt].updata;
	int changeVal = (*tree).SNode[rt].change;
	if (changeTag)
	{
		//下放change
		(*tree).SNode[rt << 1].updata = true;
		(*tree).SNode[rt << 1].change = changeVal;
		(*tree).SNode[rt << 1 | 1].updata = true;
		(*tree).SNode[rt << 1 | 1].change = changeVal;
		//清除lazy
		(*tree).SNode[rt << 1].lazy = 0;
		(*tree).SNode[rt << 1 | 1].lazy = 0;
		//更新sum
		int ln = mid - l + 1, rn = r - mid;
		(*tree).SNode[rt << 1].sum = ln * changeVal;
		(*tree).SNode[rt << 1 | 1].sum = rn * changeVal;
		//最后,重置rt节点的change标志
		(*tree).SNode[rt].updata = false;
	}
	//lazy下放
	int lazyTag = (*tree).SNode[rt].lazy;
	if (lazyTag)
	{
		int l = (*tree).SNode[rt].l, r = (*tree).SNode[rt].r, mid = l + (r - l) / 2;
		//下放lazy
		(*tree).SNode[rt << 1].lazy += (*tree).SNode[rt].lazy; 
		(*tree).SNode[rt << 1 | 1].lazy += (*tree).SNode[rt].lazy; 
		//更新sum
		int ln = mid - l + 1, rn = r - mid; 
		(*tree).SNode[rt << 1].sum += ln * lazyTag;
		(*tree).SNode[rt << 1 | 1].sum += rn * lazyTag;
		//最后,清空rt节点的lazy
		(*tree).SNode[rt].lazy = 0;
	}
}
//建树,根据arr数组填充(初始化)tree的sum信息
void Bulid(SegTree* tree, int* arr, int l, int r, int rt) 
{
/* 参数说明:
	tree表示要操作的线段树,将arr数组中的内容填充到tree中
	l, r表示当前节点在arr数组的区间映射
	rt表示当前根节点的编号(下标) 
*/
	//暴力检查
	assert(tree != NULL);
	assert(arr != NULL);
	assert(l > 0);
	assert(r < MAXN);
	assert(rt > 0 && rt < MAXN << 2);
	//不论什么情况,先填充tree的l和r
	(*tree).SNode[rt].l = l;
	(*tree).SNode[rt].r = r;
	//l==r时,表示走到了可以表示一个点的区间位置,其l/r就是对应arr数组的下标,rt就是我们线段树的节点下标
	if (l == r)
	{
		(*tree).SNode[rt].sum = arr[l];
		return;
	}
	//如果当前区间不是一个点,则分别再对两个孩子节点Build
	int mid = l + (r - l) / 2; //mid是两个孩子节点的分界点
	Bulid(tree, arr, l, mid, rt << 1);
	Bulid(tree, arr, mid + 1, r, rt << 1 | 1);
	//最后不要忘了更新当前rt节点的数据(即sum的值)
	PushUp(tree, rt);
}
//区间增加,将tree树的[L,R]区间的内容增时加C
void Add(SegTree* tree, int L, int R, int C, int rt) //L,R表示操作区间,rt表示根节点的编号(下标)
{
	//暴力检查
	assert(tree != NULL);
	assert(L > 0);
	assert(R < MAXN);
	assert(rt > 0 && rt < MAXN << 2);
	//[L,R]完全包含[l,r]时,当前rt节点直接lazy增加
	int l = (*tree).SNode[rt].l, r = (*tree).SNode[rt].r; 
	if (L <= l && R >= r)
	{
		(*tree).SNode[rt].lazy += C; 
		(*tree).SNode[rt].sum += (r - l + 1) * C; 
		return;
	}
	//rt位置lazy/change下放
	PushDown(tree, rt);
	//根据左右孩子的[l,r]是否与[L,R]有交集,选择是否进行“任务下发”
	int mid = l + (r - l) / 2;
	if (L <= mid)
		Add(tree, L, R, C, rt << 1);
	if (R >= mid + 1)
		Add(tree, L, R, C, rt << 1 | 1);
	//最后,更新rt节点元素
	PushUp(tree, rt);
}
//区间更新,将tree树的[L,R]区间的内容更新为C
void Updata(SegTree* tree, int L, int R, int C, int rt) //L,R表示操作区间,rt表示根节点的编号(下标)
{
	//暴力检查
	assert(tree != NULL);
	assert(L > 0);
	assert(R < MAXN);
	assert(rt > 0 && rt < MAXN << 2);
	//[L,R]完全包含[l,r]时,当前rt节点直接updata更新
	int l = (*tree).SNode[rt].l, r = (*tree).SNode[rt].r;
	if (L <= l && R >= r)
	{
		(*tree).SNode[rt].updata = true; //当前节点标志为已更新
		(*tree).SNode[rt].change = C;    //更新值设为C
		(*tree).SNode[rt].lazy = 0;      //将之前的lazy标志清空
		(*tree).SNode[rt].sum = (r - l + 1) * C; //更新sum值
		return;
	}
	//rt位置lazy/change下放
	PushDown(tree, rt);
	//根据左右孩子的[L,R]是否与[L,R]有交集,选择是否进行“任务下发”
	int mid = l + (r - l) / 2;
	if (L <= mid)
		Updata(tree, L, R, C, rt << 1);
	if (R >= mid + 1)
		Updata(tree, L, R, C, rt << 1 | 1);
	//最后,更新rt节点元素
	PushUp(tree, rt);

}
//区间查询,查询[L,R]区间的sum值
int QuerySum(const SegTree* tree, int L, int R, int rt)//L,R表示操作区间,rt表示根节点的编号(下标) 
{
	int ans = 0;
	//暴力检查
	assert(tree != NULL);
	assert(L > 0);
	assert(R < MAXN);
	assert(rt > 0 && rt < MAXN << 2);
	//[L,R]完全包含[l,r]时,当前rt节点直接lazy增加
	int l = (*tree).SNode[rt].l, r = (*tree).SNode[rt].r;
	if (L <= l && R >= r)
	{	
		return (*tree).SNode[rt].sum;
	}
	//rt位置lazy/change下放
	PushDown(tree, rt);
	//根据左右孩子的[l,r]是否与[L,R]有交集,选择是否进行“任务下发”
	int mid = l + (r - l) / 2;
	if (L <= mid)
		ans += QuerySum(tree, L, R, rt << 1); 
	if (R >= mid + 1)
		ans += QuerySum(tree, L, R, rt << 1 | 1); 
	//最后,更新rt节点元素
	return ans; 
}

//测试用例
void test01()
{
	SegTree tree;
	memset(&tree, 0, sizeof(SegTree));
	tree.SNode[2].sum = 5;
	tree.SNode[3].sum = 8;
	PushUp(&tree, 1);
	printf("%d\n", tree.SNode[1].sum);
}
void test02()
{ 
	SegTree tree;
	memset(&tree, 0, sizeof(SegTree)); 
	int arr[MAXN] = { 0,1,2,3,4,5,6,7,8 };
	Bulid(&tree, arr, 1, MAXN - 1, 1); //rt是tree的下标
	Add(&tree, 1, 8, 1, 1);  
	Add(&tree, 2, 6, 1, 1);  
	printf("%d\n", tree.SNode[1].sum);
	int tmp; 
}
void test03()
{
	SegTree tree;
	memset(&tree, 0, sizeof(SegTree));
	int arr[MAXN] = { 0,1,2,3,4,5,6,7,8 };
	Bulid(&tree, arr, 1, MAXN - 1, 1); //rt是tree的下标
	//Add(&tree, 1, 8, 1, 1);
	//Add(&tree, 2, 6, 1, 1);
	Updata(&tree, 2, 6, 1, 1); 
	//Add(&tree, 1, 8, 1, 1);
	Add(&tree, 2, 6, 1, 1);
	printf("%d\n", tree.SNode[1].sum);
	int test_query = QuerySum(&tree, 3, 7, 1); 
	printf("%d\n", test_query);
	int tmp;
}

//主函数
int main()
{
	test01();
	test02();
	test03();
	return 0;
}

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

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

相关文章

Android 车载值不值得入手学?

前言 随着智能车的不断普及和智能化程度的提高&#xff0c;车载系统也在逐步升级和演进&#xff0c;越来越多的汽车厂商开始推出采用Android系统的车载设备&#xff0c;这为Android车载开发提供了广泛的市场需求。 其次&#xff0c;随着人工智能技术的发展和应用&#xff0c;…

Linux : 安装源码包

安装源码包之前我们要准备好yum环境&#xff0c;或者使用默认上网下载的yum仓库或者查看&#xff1a;Linux&#xff1a;rpm查询安装 && yum安装_鲍海超的博客-CSDN博客 准备离线yum仓库 &#xff0c;默认的需要在有网环境下才能去网上下载 其次就是安装 gcc make 准…

UDP协议 sendto 和 recvfrom 浅析与示例

UDP&#xff08;user datagram protocol&#xff09;用户数据报协议&#xff0c;属于传输层。 UDP是面向非连接的协议&#xff0c;它不与对方建立连接&#xff0c;而是直接把数据报发给对方。UDP无需建立类如三次握手的连接&#xff0c;使得通信效率很高。因此UDP适用于一次传…

Kyligence Zen 一站式指标平台体验——“绝对实力”的指标分析和管理工具——入门体验评测

&#x1f996;欢迎观阅本本篇文章&#xff0c;我是Sam9029 文章目录 前言Kyligence Zen 是什么Kyligence Zen 能做什么Kyligence Zen 优势在何处 正文注册账号平台功能模块介绍指标图表新建指标指标模板 目标仪表盘数据设置 实际业务体验---使用官网数据范例使用流程归因分析指…

MySQL --- 多表设计

关于单表的操作(包括单表的设计、单表的增删改查操作)我们就已经学习完了。接下来我们就要来学习多表的操作&#xff0c;首先来学习多表的设计。 项目开发中&#xff0c;在进行数据库表结构设计时&#xff0c;会根据业务需求及业务模块之间的关系&#xff0c;分析并设计表结构…

ChatGPT-4怎么对接-ChatGPT-4强化升级了哪些功能

ChatGPT-4怎么使用 使用ChatGPT-4&#xff0c;需要通过OpenAI的API接口来对接ChatGPT-4。OpenAI是一个人工智能公司&#xff0c;为开发者提供多个API接口&#xff0c;包括自然语言处理&#xff0c;图像处理等。ChatGPT-4是OpenAI开发的最新版本的聊天式对话模型&#xff0c;可…

React antd Form item「受控组件与非受控组件」子组件 defaultValue 不生效等问题总结

一、为什么 Form.Item 下的子组件 defaultValue 不生效&#xff1f; 当你为 Form.Item 设置 name 属性后&#xff0c;子组件会转为受控模式。因而 defaultValue 不会生效。你需要在 Form 上通过 initialValues 设置默认值。name 字段名&#xff0c;支持数组 类型&#xff1a;N…

2.存储器层次系统

存储器 随机访问存储器 RAM&#xff08;随机存储器&#xff09; SRAM 双稳态触发器&#xff0c;有电就保持不变&#xff0c;干扰消除后时会恢复到稳定值&#xff0c;晶体管多因此密集度低 DRAM 每个位存储为对一个电容的充电&#xff0c;对干扰敏感&#xff0c;漏电所以需要刷…

静态数码管

静态数码管 1、简介工作方式数码管静态显示原理 2、硬件设计3、软件设计4、 1、简介 一般共阳极数码管更为常用 好处&#xff1a;将驱动数码管的工作交到公共端&#xff08;一般接驱动电源&#xff09;&#xff0c;加大驱动电源的功率自然要比加大IC芯片I/O口的驱动电流简单许…

【python 生成器】零基础也能轻松掌握的学习路线与参考资料

一、学习路线 了解生成器的概念和作用 首先&#xff0c;需要明确生成器的概念和作用&#xff0c;生成器是一种特殊的迭代器&#xff0c;它可以在循环中逐个地产生值&#xff0c;而不是一次性将所有的值产生出来。它的作用是使程序更加高效&#xff0c;达到节省内存等的效果。…

Linux 入门

文章目录 一、概述二、安装CentOS下载地址VMware下载地址 三、linux文件与目录结构Linux系统中一切皆文件Linux目录结构 四、VI/VIM 编辑器vi/vim是什么一般模式常用语法键盘图编辑模式指令模式 五、网络配置六、远程登陆七、系统管理Linux 中的进程和服务service 服务管理chkc…

几种常见的电源防反接电路

电源防反接&#xff0c;也即是防止电源的正负极搞反而导致电路损坏&#xff0c;例如你采用的是标准的DC口&#xff0c;那么没什么必要加入此种电路。而如果采用的是非常规的&#xff0c;如自定义的接插件等&#xff0c;那么就很有必要了。 举个例子&#xff1a;小编以前就采用…

企业在线制作帮助中心,选择:语雀、石墨、Baklib哪个好?

在当今互联网时代&#xff0c;越来越多的企业开始将帮助中心建设在线化。在线帮助中心的好处不仅可以提高用户的使用体验&#xff0c;也可以提高企业的工作效率。然而&#xff0c;选择一个合适的在线制作帮助中心工具却并不是一件容易的事情。在众多的在线制作帮助中心工具中&a…

Python3 入门教程||Python3 SMTP发送邮件||Python3 多线程

Python3 SMTP发送邮件 在Python3 中应用的SMTP&#xff08;Simple Mail Transfer Protocol&#xff09;即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则&#xff0c;由它来控制信件的中转方式。 python的 smtplib 提供了一种很方便的途径发送电子邮件。它对…

[cryptoverse CTF 2023] crypto部分

没打,完事作作题. Warmup 1 Decode the following ciphertext: GmvfHt8Kvq16282R6ej3o4A9Pp6MsN. Remember: CyberChef is your friend. Another great cipher decoding tool is Ciphey. 热身一下就凉,问了别人,用ciphey说是能自动解,但是安装报错 rot13base58 这个没有自动的…

JavaCollection集合:概述、体系特点、常用API、遍历方式

一、集合概述 集合和数组都是容器 数组 特点&#xff1a;数组定义完成并启动后&#xff0c;类型确定、长度固定。 劣势&#xff1a;在进行增删数据操作的时候&#xff0c;数组是不太合适的&#xff0c;增删数据都需要放弃原有数组或者移位。 使用场景&#xff1a;当业务数…

JMeter 常用的几种断言方法,你会了吗?

JMeter是一款常用的负载测试工具&#xff0c;通过模拟多线程并发请求来测试系统的负载能力和性能。在进行性能测试时&#xff0c;断言&#xff08;Assertion&#xff09;是非常重要的一部分&#xff0c;可以帮助我们验证测试结果的正确性。下面介绍JMeter常用的几种断言方法。 …

MySQL 运算符解析

1.算术运算符 算术运算符主要用于数学运算&#xff0c;其可以连接运算符前后的两个数值或表达式&#xff0c;对数值或表达式进行加 &#xff08;&#xff09;、减&#xff08;-&#xff09;、乘&#xff08;*&#xff09;、除&#xff08;/&#xff09;和取模&#xff08;%&…

K8S:K8S部署常见错误及解决方法

目录 1、node节点kubelet服务起不来 2、安装cni网络插件时 kubectl get node master和node一直noready①有延时&#xff0c;需要等待10分钟左右&#xff0c;超过15分钟则有问题 3、部署报错kubectl get nodes No resources found 4、k8s部署报错error&#xff1a;kubectl ge…

OpenCV 直方图统计函数 cv::calcHist算是彻底弄明白了

参数说明 void calcHist( const Mat* images, int nimages,const int* channels, InputArray mask,OutputArray hist, int dims, const int* histSize,const float** ranges, bool uniform true, bool accumulate false );images 图像数组。每个图像的大小要一致&#xff0c…