线段树能解决多少问题?

news2025/2/2 18:02:08

背景

给一个两个数组,其中一个数组是 A [1,2,3,4],另外一个数组是 B [5,6,7,8]。让你求两个数组合并后的大数组的:

  • 最大值
  • 最小值
  • 总和

这题是不是很简单?我们直接可以很轻松地在 O(m+n) 的时间解决,其中 m 和 n 分别为数组 A 和 B 的大小。

那如果我可以「修改」 A 和 B 的某些值,并且我要求「很多次」最大值,最小值和总和呢?

朴素的思路是原地修改数组,然后 O(m+n) 的时间重新计算。显然这并没有利用之前计算好的结果,效率是不高的。 那有没有效率更高的做法?

有!线段树就可以解决。

线段树是什么?

线段树本质上就是一棵树。更准确地说,它是一颗二叉树,而且它是一颗平衡二叉树。关于为什么是平衡二叉树,我们后面会讲,这里大家先有这样一个认识。

虽然是一棵二叉树,但是线段树我们通常使用数组来模拟树结构,而不是传统的定义 TreeNode。

 一方面是因为实现起来容易,另外一方面是因为线段树其实是一颗完全二叉树,因此使用数组直接模拟会很高效。

解决什么问题

正如它的名字,线段树和线段(区间)有关。线段树的每一个树节点其实都存储了一个「区间(段)的信息」。然后这些区间的信息如果「满足一定的性质」就可以用线段树来提高性能。

那:

  1. 究竟是什么样的性质?
  2. 如何提高的性能呢?

究竟是什么样的性质?

比如前面我们提到的最大值,最小值以及求和就满足这个「一定性质」。即我可以根据若干个(这里是两个)子集推导出子集的并集的某一指标。

以上面的例子来说,我们可以将数组 A 和 数组 B 看成两个集合。那么:集合 A 的最大值和集合 B 的最大值已知,我们可以直接通过 max(Amax, Bmax) 求得集合 A 与集合 B 的并集的最大值。其中 Amax 和 Bmax 分别为集合 A 和集合 B 的最大值。最小值和总和也是一样的,不再赘述。因此如果统计信息满足这种性质,我们就可以可以使用线段树。但是要不要使用,还是要看用了线段树后,是否能提高性能。

如何提高的性能呢?

关于提高性能,我先卖一个关子,等后面讲完实现的时候,我们再聊。

实现

以文章开头的求和为例。

我们可以将区间 A 和 区间 B 分别作为一个树的左右节点,并将 A 的区间和与 B 的区间和分别存储到左右子节点中。

接下来,将 A 的区间分为左右两部分,同理 B 也分为左右两部分。不断执行此过程直到无法继续分。

 总结一下就是将区间不断一分为二,并将区间信息分别存储到左右节点。如果是求和,那么区间信息就是区间的和。这个时候的线段树大概是这样的:

❝ 蓝色字体表示的区间和。

注意,这棵树的所有叶子节点一共有 n 个(n 为原数组长度),并且每一个都对应到原数组某一个值。

体现到代码上也很容易。 直接使用「后续遍历」即可解决。这是因为,我们需要知道左右节点的统计信息,才能计算出当前节点的统计信息。

和二叉堆的表示方式一样,我们可以用数组表示树,用 2 * i + 12 * 1 + 2 来表示左右节点的索引,其中 i 为当前节点对应在 tree 上的索引。

❝ tree 是用来构建线段树的数组,和二叉树类似。只不过 tree[i] 目前存的是区间信息罢了。

上面我描述建树的时候有明显的递归性,因此我们可以递归的建树。具体来说,可以定义一个 build(tree_index, l, r) 方法 来建树。其中 l 和 r 就是对应区间的左右端点,这样 l 和 r 就可以唯一确定一个区间。 tree_index 其实是用来标记当前的区间信息应该被更新到 tree 数组的哪个位置。

我们在 tree 上存储区间信息,那么最终就可以用 tree[tree_index] = .... 来更新区间信息啦。

核心代码:

def build(self, tree_index:int, l:int, r:int):
    '''
    递归创建线段树
    tree_index : 线段树节点在数组中位置
    l, r : 该节点表示的区间的左,右边界
    '''
    if l == r:
        self.tree[tree_index] = self.data[l]
        return
    mid = (l+r) // 2 # 区间中点,对应左孩子区间结束,右孩子区间开头
    left, right = 2 * tree_index + 1, 2 * tree_index + 2 # tree_index 的左右子树索引
    self.build(left, l, mid)
    self.build(right, mid+1, r)
    # 典型的后序遍历
    # 区间和使用加法即可,如果不是区间和要改下面这行代码
    self.tree[tree_index] = self.tree[left] + self.tree[right]

上面代码的数组 self.tree[i] 其实就是用来存类似上图中蓝色字体的区间和。「每一个区间都在 tree 上存有它一个位置,存它的区间和」

「复杂度分析」

  • 时间复杂度:由递推关系式 T(n) = 2*T(n/2) + 1,因此时间复杂度为 O(n)
  • 空间复杂度:tree 的大小和 n 同阶,因此空间复杂度为 O(n)

终于把树建好了,但是知道现在一点都没有高效起来。我们要做的是高效处理「频繁更新情况下的区间查询」

那基于这种线段树的方法,如果更新和查询区间信息如何做呢?

区间查询

先回答简单的问题「区间查询」原理是什么。

如果查询一个区间的信息。这里也是使用后序遍历就 ok 了。比如我要找一个区间 [l,r] 的区间和。

那么如果当前左节点:

  • 完整地落在 [l,r] 内。比如 [2,3] 完整地落在 [1,4] 内。 我们直接将 tree 中左节点对于的区间和取出来备用,不妨极为 lsum。
  • 部分落在 [l,r] 内。比如 [1,3] 部分落在 [2,4]。这个时候我们继续递归,直到完整地落在区间内(上面的那种情况),这个时候我们直接将 tree 中左节点对于的区间和取出来备用
  • 将前面所有取出来备用的值加起来就是答案

 右节点的处理也是一样的,不再赘述。

「复杂度分析」

  • 时间复杂度:查询不需要在每个时刻都处理两个叶子节点,实际上处理的次数大致和树的高度一致。而树是平衡的,因此复杂度为 O(logn)

或者由递推关系式 T(n) = T(n/2) + 1,因此时间复杂度为 O(logn)

区间修改

那么如果我修改了 A[1] 为 1 呢?

如果不修改 tree,那么显然查询的区间只要包含了 A[1] 就一定是错的,比如查询区间 [1,3] 的和 就会得到错误的答案。因此我们要在修改了 A[1] 的时候同时去修改 tree。

问题在于「我们要修改哪些 tree 的值,修改为多少呢?」

首先回答第一个问题,修改哪些 tree 的值呢?

我们知道,线段树的叶子节点都是原数组上的值,也是说,线段树的 n 个叶子节点对应的就是原数组。因此我们首先要「找到我们修改的位置对应的那个叶子节点,将其值修改掉。」

这就完了么?

没有完。实际上,我们修改的叶子节点的所有父节点以及祖父节点(如果有的话)都需要改。也就是说我们需要「从这个叶子节点不断冒泡到根节点,并修改沿途的区间信息」

❝ 这个过程和浏览器的事件模型是类似的

接下来回答最后一个问题,具体修改为多少?

对于求和,我们需要首先将叶子节点改为修改后的值,另外所有叶子节点到根节点路径上的点的区间和都加上 delta,其中 delta 就是改变前后的差值。

❝ 求最大最小值如何更新?大家自己思考一下。

修改哪些节点,修改为多少的问题都解决了,那么代码实现就容易了。

「复杂度分析」

  • 时间复杂度:修改不需要在每个时刻都处理两个叶子节点,实际上处理的次数大致和树的高度一致。而树是平衡的,因此复杂度为 O(logn)

或者由递推关系式 T(n) = T(n/2) + 1,因此时间复杂度为 O(logn)

大家可以结合后面的代码理解这个复杂度。

代码

class SegmentTree:
    def __init__(self, data:List[int]):
        '''
        data: 传入的数组
        '''
        self.data = data
        self.n = len(data)
        #  申请 4 倍 data 长度的空间来存线段树节点
        self.tree = [None] * (4 * self.n) # 索引 i 的左孩子索引为 2i+1,右孩子为 2i+2
        if self.n:
            self.build(0, 0, self.n-1)
    # 本质就是一个自底向上的更新过程
    # 因此可以使用后序遍历,即在函数返回的时候更新父节点。
    def update(self, tree_index, l, r, index):
        '''
        tree_index: 某个根节点索引
        l, r : 此根节点代表区间的左右边界
        index : 更新的值的索引
        '''
        if l == r==index :
            self.tree[tree_index] = self.data[index]
            return
        mid = (l+r)//2
        left, right = 2 * tree_index + 1, 2 * tree_index + 2
        if index > mid:
            # 要更新的区间在右子树
            self.update(right, mid+1, r, index)
        else:
            # 要更新的区间在左子树 index<=mid
            self.update(left, l, mid, index)
        # 查询区间一部分在左子树一部分在右子树
        # 区间和使用加法即可,如果不是区间和要改下面这行代码
        self.tree[tree_index] = self.tree[left] + self.tree[right]

    def updateSum(self,index:int,value:int):
        self.data[index] = value
        self.update(0, 0, self.n-1, index)
    def query(self, tree_index:int, l:int, r:int, ql:int, qr:int) -> int:
        '''
        递归查询区间 [ql,..,qr] 的值
        tree_index : 某个根节点的索引
        l, r : 该节点表示的区间的左右边界
        ql, qr: 待查询区间的左右边界
        '''
        if l == ql and r == qr:
            return self.tree[tree_index]

        # 区间中点,对应左孩子区间结束,右孩子区间开头
        mid = (l+r) // 2
        left, right = tree_index * 2 + 1, tree_index * 2 + 2
        if qr <= mid:
            # 查询区间全在左子树
            return self.query(left, l, mid, ql, qr)
        elif ql > mid:
            # 查询区间全在右子树
            return self.query(right, mid+1, r, ql, qr)

        # 查询区间一部分在左子树一部分在右子树
        # 区间和使用加法即可,如果不是区间和要改下面这行代码
        return self.query(left, l, mid, ql, mid) + self.query(right, mid+1, r, mid+1, qr)

    def querySum(self, ql:int, qr:int) -> int:
        '''
        返回区间 [ql,..,qr] 的和
        '''
        return self.query(0, 0, self.n-1, ql, qr)

    def build(self, tree_index:int, l:int, r:int):
        '''
        递归创建线段树
        tree_index : 线段树节点在数组中位置
        l, r : 该节点表示的区间的左,右边界
        '''
        if l == r:
            self.tree[tree_index] = self.data[l]
            return
        mid = (l+r) // 2 # 区间中点,对应左孩子区间结束,右孩子区间开头
        left, right = 2 * tree_index + 1, 2 * tree_index + 2 # tree_index 的左右子树索引
        self.build(left, l, mid)
        self.build(right, mid+1, r)
        # 区间和使用加法即可,如果不是区间和要改下面这行代码
        self.tree[tree_index] = self.tree[left] + self.tree[right]

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

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

相关文章

maven的java工程获取mysql数据库数据【问题及解决过程记录】

创建数据库maven&#xff0c;指定字符集和排序规则 UTF8MB4常用的排序规则&#xff1a;utf8mb4_unicode_ci、utf8mb4_general_ci、utf8mb4_bin&#xff0c;选用哪种方式呢&#xff1f;先来分析一下&#xff1a; 1、准确性&#xff1a; &#xff08;1&#xff09;utf8mb4_unico…

ffmpeg-时间基tbn、tbc、tbr

时间基的作用 源码来自ffmpeg5.1。 时间基在ffmpeg中是通过数据结构有理数AVRational描述的。时间基为时间戳的单位&#xff0c;比如时间基tbn(AVStream.time_base)0.001秒&#xff0c;AVPacket的pts40&#xff0c;则表明该AVPacket要在tbn*pts0.04秒开始显示。 /** 代码路径…

JavaScript 网页特效

一、Offset 1.1 概述 offset > 偏移量 &#xff0c;可以动态的获取的元素的位置、大小等属性。 获得元素距离带有定位父元素的位置获得元素自身的大小(宽度高度) 返回的数值都不带单位 offset常用属性&#xff1a; 属性作用element.offsetParent返回作为该元素带有定位…

全球汽车后行业发展现状:欧洲市场保持稳健 中国产业规模增速较快

根据观研报告网发布的《2022年中国汽车后市场分析报告-市场发展格局与投资潜力研究》显示&#xff0c;汽车后市场&#xff08;AM市场&#xff09;是指汽车在销售之后维修和保养服务及其所包含的汽车零部件、汽车用品和材料的交易市场&#xff0c;它涵盖了消费者买车后所需要的一…

LeetCode 321 周赛

2485. 找出中枢整数 给你一个正整数 n &#xff0c;找出满足下述条件的 中枢整数 x &#xff1a; 1 和 x 之间的所有元素之和等于 x 和 n 之间所有元素之和。 返回中枢整数 x 。如果不存在中枢整数&#xff0c;则返回 -1 。题目保证对于给定的输入&#xff0c;至多存在一个中…

STM32单片机直流电机PID速度控制正反转控制(霍尔磁铁测速)LCD1602

实践制作DIY- GC0116-直流电机PID速度控制 一、功能说明&#xff1a; 基于STM32单片机设计-直流电机PID速度控制 功能介绍&#xff1a; STM32F103C系列最小系统LCD1602直流电机磁铁霍尔传感器MX15系列驱动模块4个按键&#xff08;速度减、速度加、开/关、正转/反转&#xff0…

【复习笔记】【嵌入式】嵌入式系统及其原理复习重点——篇二

嵌入式系统及其原理复习重点笔记 2 ARM处理器和指令集 ARM处理器简介 ARM架构与ARM处理器对应关系 V1版架构 该版架构只在原型机ARM1出现过,处理能力有限&#xff0c;其基本性能&#xff1a; 寻址空间&#xff1a;64M字节(26位)基本的数据处理指令(无乘法)字节、半字和字的…

4个封神的电脑软件,颠覆你对白嫖的认知,干货奉上

闲话少说&#xff0c;直上干货。 1、TinyWow TinyWow虽说是国外网站工具&#xff0c;但不得不承认真的无敌好用&#xff0c;收纳工具超200个&#xff0c;完全免费&#xff0c;无任何弹屏广告&#xff0c;更为良心的是&#xff0c;不需要注册登录&#xff0c;随用随走&#xff0…

如何优化大场景实时渲染?HMS Core 3D Engine这么做

在先前举办的华为开发者大会2022&#xff08;HDC&#xff09;上&#xff0c;华为通过3D数字溪村展示了自有3D引擎“HMS Core 3D Engine”&#xff08;以下简称3D Engine&#xff09;的强大能力。作为一款高性能、高画质、高扩展性的3D引擎&#xff0c;3D Engine不仅能通过实时光…

C++文件流

1、【转】string和stringstream用法总结 - 小金乌会发光&#xff0d;Z&M - 博客园 2、C&#xff1a;std::stringstream【数据类型转换、多个字符串拼接、分割字符串】_u013250861的博客-CSDN博客_c stringstream转string 3、C使用stringstream进行数据类型转换_puppylpg的…

TCP延迟应答、捎带应答、粘包问题、异常处理

TCP延迟应答、捎带应答、粘包问题、异常处理一、延迟应答二、捎带应答三、面向字节流 -- 粘包问题四、TCP中的异常处理五、补充一、延迟应答 上篇博客我们讲到TCP滑动窗口、流量控制、拥塞控制。 如果接收数据的主机立刻返回ACK应答&#xff0c;这时候返回的窗口可能比较小。…

[ vulhub漏洞复现篇 ] Airflow dag中的命令注入漏洞复现 CVE-2020-11978

&#x1f36c; 博主介绍 &#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 _PowerShell &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【数据通信】 【通讯安全】 【web安全】【面试分析】 &#x1f389;点赞➕评论➕收藏 养成习…

web前端-javascript-function函数的arguments对象(类数组对象,它也可以通过索引来操作数据,也可以获取长度)

arguments 对象 1. 引出 arguments 在调用 function 函数时&#xff0c;浏览器每次都会传递进两个隐含的参数 函数的上下文对象 this封装实参的对象 arguments 2. 说明 arguments 是一个类数组对象,它也可以通过索引来操作数据&#xff0c;也可以获取长度在调用函数时&#…

ACL会议介绍 - Call for Main Conference Papers

The 61st Annual Meeting of the Association for Computational Linguistics Toronto, Canada July 9-14, 2023 网址&#xff1a;The 61st Annual Meeting of the Association for Computational Linguistics - ACL 2023 目录 征集主要会议文件 Submission Topics 主题轨迹…

Stm32标准库函数3——BlueTooth 蓝牙通讯测试 Stm32中继

//在使用本程序前&#xff0c;先将模块与手机端匹配成功&#xff0c;波特率38400 //串口1&#xff08;A9、A10&#xff09;接电脑&#xff0c;串口2&#xff08;A2、A3&#xff09;接蓝牙模块 //所有的波特率都为38400&#xff0c;蓝牙的供电为3.3-5v //程序功能&#xff0c;转…

Discrete Optimization课程笔记(4)—混合整数规划

目录​​​​​​​ 1.MIP介绍(Mixed Integer Program) Case1: Warehouse Location Case2: Knapsack Problem(Branch and Bound) 2.MIP模型(modeling) Case3: Coloring Problem(Big-M Transformation) 3.割平面法(Cutting planes) 4.多面体切割(Polyhedral Cuts) Cas…

前端工程化VUE-cli

六 前端工程化vue-cli Vue是渐近式框架&#xff0c;你可以用它一个功能&#xff0c;也可以用全家桶。前面的章节中&#xff0c;我们是在html中引入vue.js&#xff0c;只用它核心的数据绑定功能。但基于vue的扩展还有很多&#xff0c;比如vueRouter&#xff0c;axios&#xff0…

Base64编码剖析

文章目录Base64编码概述Base64原理索引表如何转换&#xff1f;Java实操Java代码实现Base64参考文章Base64编码概述 百度百科中对Base64有一个很好的解释&#xff1a;“Base64是网络上最常见的用于传输8Bit字节码的编码方式之一&#xff0c;Base64就是一种基于64个可打印字符来…

【面试题】5年前端 - 历时1个月收获7个offer

大厂面试题分享 面试题库 前端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★ 地址&#xff1a;前端面试题库 前言 省流&#xff1a;最终拿到了58、UMU、便利蜂、虾皮、快手、腾讯、字节的offer。 金三银四面试的, 这次整体面试通过率还挺高的, …

深入解读云场景下的网络抖动

一、网络抖动背景 延时高&#xff0c;网络卡&#xff0c;卡住了美好&#xff01; 应用抖&#xff0c;业务惊&#xff0c;惊扰了谁的心&#xff1f; 当你在观看世界杯梅西主罚点球突然视频中断了几秒钟 当你在游戏中奋力厮杀突然手机在转圈圈无法响应 当你守候多时为了抢一…