线段树(Segment Tree)和树状数组

线段树(Segment Tree)和树状数组

    • 线段树的实现
      • 链式:
      • 数组实现
  • 解题思路
    • 树状数组

线段树是 二叉树结构 的衍生,用于高效解决区间查询和动态修改的问题,其中区间查询的时间复杂度为 O(logN),动态修改单个元素的时间复杂度为x O(logN)。
这棵二叉树的叶子节点是数组中的元素,非叶子节点就是索引区间(线段)的汇总信息.
在这里插入图片描述
线段树结构可以有多种变体及复杂的优化,我们这里只聚焦最核心的两个 API:

class SegmentTree {// 构造函数,给定一个数组,初始化线段树,时间复杂度 O(N)// merge 是一个函数,用于定义 query 方法的行为// 通过修改这个函数,可以让 query 函数返回区间的元素和、最大值、最小值等public SegmentTree(int[] nums, Function<Integer, Integer> merge) {}// 查询闭区间 [i, j] 的元素和(也可能是最大最小值,取决于 merge 函数),时间复杂度 O(logN)public int query(int i, int j) {}// 更新 nums[i] = val,时间复杂度 O(logN)public void update(int i, int val) {}
}
  • suffixMin[i] 可以在 O(1) 时间内查询; nums[i…] 后缀的最小值线段树的 query 方法不仅可以查询后缀,还可以查询任意 [i, j] 区间,时间复杂度均为
    O(logN)。
  • 当底层 nums 数组中的任意元素变化时,需要重新计算 suffixMin 数组,时间复杂度为 O(N);而线段树的 update 方法可以在 O(logN) 时间内完成元素的修改。
  • 线段树不仅仅支持计算区间的最小值,只要修改 merge 函数,就可以支持计算区间元素和、最大值、乘积等。

线段树的实现

线段树是一种二叉树,所以可以用二叉树的方式实现,包括链式实现和数组实现两种:

链式:

from typing import Callable# 线段树节点
class SegmentNode:# 该节点表示的区间范围 [l, r]def __init__(self, merge_val: int, l: int, r: int):# [l, r] 区间元素的聚合值(如区间和、区间最大值等)self.l = lself.r = rself.merge_val = merge_valself.left = Noneself.right = Noneclass SegmentTree:def __init__(self, nums: list, merger: Callable[[int, int], int]):# 创建线段树# 输入数组 nums 和一个聚合函数 merger,merger 用于计算区间的聚合值self.merger = mergerself.root = self.build(nums, 0, len(nums) - 1)# 定义:将 nums[l..r] 中的元素构建成线段树,返回根节点def build(self, nums: list, l: int, r: int) -> SegmentNode:# 区间内只有一个元素,直接返回if l == r:return SegmentNode(nums[l], l, r)# 从中间切分,递归构建左右子树mid = l + (r - l) // 2left = self.build(nums, l, mid)right = self.build(nums, mid + 1, r)# 根据左右子树的聚合值,计算当前根节点的聚合值node = SegmentNode(self.merger(left.merge_val, right.merge_val), l, r)# 组装左右子树node.left = leftnode.right = rightreturn nodedef update(self, index: int, value: int):self._update(self.root, index, value)def _update(self, node: SegmentNode, index: int, value: int):if node.l == node.r:# 找到了目标叶子节点,更新值node.merge_val = valuereturnmid = node.l + (node.r - node.l) // 2if index <= mid:# 若 index 较小,则去左子树更新self._update(node.left, index, value)else:# 若 index 较大,则去右子树更新self._update(node.right, index, value)# 后序位置,左右子树已经更新完毕,更新当前节点的聚合值node.merge_val = self.merger(node.left.merge_val, node.right.merge_val)def query(self, qL: int, qR: int) -> int:return self._query(self.root, qL, qR)def _query(self, node: SegmentNode, qL: int, qR: int) -> int:if qL > qR:raise ValueError("Invalid query range")if node.l == qL and node.r == qR:# 命中了目标区间,直接返回return node.merge_val# 未直接命中区间,需要继续向下查找mid = node.l + (node.r - node.l) // 2if qR <= mid:# node.l <= qL <= qR <= mid# 目标区间完全在左子树中return self._query(node.left, qL, qR)elif qL > mid:# mid < qL <= qR <= node.r# 目标区间完全在右子树中return self._query(node.right, qL, qR)else:# node.l <= qL <= mid < qR <= node.r# 目标区间横跨左右子树# 将查询区间拆分成 [qL, mid] 和 [mid + 1, qR] 两部分,分别向左右子树查询# 最后将左右子树的查询结果合并return self.merger(self._query(node.left, qL, mid),self._query(node.right, mid + 1, qR))# Example usage
if __name__ == "__main__":arr = [1, 3, 5, 7, 9]# 示例,创建一棵求和线段树st = SegmentTree(arr, lambda a, b: a + b)print(st.query(1, 3)) # 3 + 5 + 7 = 15st.update(2, 10)print(st.query(1, 3)) # 3 + 10 + 7 = 20

数组实现

from typing import Callableclass ArraySegmentTree:# 用数组存储线段树结构def __init__(self, nums: list[int], merger: Callable[[int, int], int]):# 元素个数self.n = len(nums)self.merger = merger# 分配 4 倍数组长度的空间,存储线段树self.tree = [0] * (4 * self.n)self.build(nums, 0, self.n - 1, 0)# 定义:对 nums[l..r] 区间的元素构建线段树,rootIndex 是根节点def build(self, nums: list[int], l: int, r: int, rootIndex: int):if l == r:# 区间内只有一个元素,设置为叶子节点self.tree[rootIndex] = nums[l]return# 从中间切分,递归构建左右子树mid = l + (r - l) // 2leftRootIndex = self.leftChild(rootIndex)rightRootIndex = self.rightChild(rootIndex)# 递归构建 nums[l..mid],根节点为 leftRootIndexself.build(nums, l, mid, leftRootIndex)# 递归构建 nums[mid+1..r],根节点为 rightRootIndexself.build(nums, mid + 1, r, rightRootIndex)# 后序位置,左右子树已经构建完毕,更新当前节点的聚合值self.tree[rootIndex] = self.merger(self.tree[leftRootIndex], self.tree[rightRootIndex])def update(self, index: int, value: int):self._update(0, self.n - 1, 0, index, value)# 当前节点为 rootIndex,对应的区间为 [l, r]# 去子树更新 nums[index] 为 valuedef _update(self, l: int, r: int, rootIndex: int, index: int, value: int):if l == r:# 找到了目标叶子节点,更新值self.tree[rootIndex] = valuereturnmid = l + (r - l) // 2if index <= mid:# 若 index 较小,则去左子树更新self._update(l, mid, self.leftChild(rootIndex), index, value)else:# 若 index 较大,则去右子树更新self._update(mid + 1, r, self.rightChild(rootIndex), index, value)# 后序位置,左右子树已经更新完毕,更新当前节点的聚合值self.tree[rootIndex] = self.merger(self.tree[self.leftChild(rootIndex)],self.tree[self.rightChild(rootIndex)])def query(self, qL: int, qR: int) -> int:if qL < 0 or qR >= self.n or qL > qR:raise ValueError(f"Invalid range: [{qL}, {qR}]")return self._query(0, self.n - 1, 0, qL, qR)def _query(self, l: int, r: int, rootIndex: int, qL: int, qR: int) -> int:if qL == l and r == qR:# 命中了目标区间,直接返回return self.tree[rootIndex]mid = l + (r - l) // 2leftRootIndex = self.leftChild(rootIndex)rightRootIndex = self.rightChild(rootIndex)if qR <= mid:# node.l <= qL <= qR <= mid# 目标区间完全在左子树中return self._query(l, mid, leftRootIndex, qL, qR)elif qL > mid:# mid < qL <= qR <= node.r# 目标区间完全在右子树中return self._query(mid + 1, r, rightRootIndex, qL, qR)else:# node.l <= qL <= mid < qR <= node.r# 目标区间横跨左右子树# 将查询区间拆分成 [qL, mid] 和 [mid + 1, qR] 两部分,分别向左右子树查询return self.merger(self._query(l, mid, leftRootIndex, qL, mid),self._query(mid + 1, r, rightRootIndex, mid + 1, qR))def leftChild(self, pos: int) -> int:return 2 * pos + 1def rightChild(self, pos: int) -> int:return 2 * pos + 2if __name__ == "__main__":arr = [1, 3, 5, 7, 9]# 示例,创建一棵求和线段树st = ArraySegmentTree(arr, lambda a, b: a + b)print(st.query(1, 3)) # 3 + 5 + 7 = 15st.update(2, 10)print(st.query(1, 3)) # 3 + 10 + 7 = 20

307区域和检索

解题思路

针对不同的题目,我们有不同的方案可以选择(假设我们有一个数组):

1.数组不变,求区间和:「前缀和」、「树状数组」、「线段树」
2.多次修改某个数(单点),求区间和:「树状数组」、「线段树」
3.多次修改某个区间,输出最终结果:「差分」
4.多次修改某个区间,求区间和:「线段树」、「树状数组」(看修改区间范围大小)
5.多次将某个区间变成同一个数,求区间和:「线段树」、「树状数组」(看修改区间范围大小)
这样看来,「线段树」能解决的问题是最多的,那我们是不是无论什么情况都写「线段树」呢?

答案并不是,而且恰好相反,只有在我们遇到第 4 类问题,不得不写「线段树」的时候,我们才考虑线段树。

因为「线段树」代码很长,而且常数很大,实际表现不算很好。我们只有在不得不用的时候才考虑「线段树」。

总结一下,我们应该按这样的优先级进行考虑:

简单求区间和,用「前缀和」
多次将某个区间变成同一个数,用「线段树」
其他情况,用「树状数组」

作者:宫水三叶
链接:https://leetcode.cn/problems/range-sum-query-mutable/solutions/632515/guan-yu-ge-lei-qu-jian-he-wen-ti-ru-he-x-41hv/
来源:力扣(LeetCode)
著作权归作者所有。

树状数组

在这里插入图片描述
树状数组是用tree[i]维护从某个值开始到i的区间的元素和,下标从1开始
prefixsum[i]表示前i个数的前缀和,这样可以简单求[left,right]区间的元素和

sum[left,right] = prefixsum[right+1]-prefixsum[left]

prefixsum[i]被分解为几个小区间tree[i]的和,

#倒着分解,每次区间的长度为二进制的最低位bitlower 可以用`i&-i`计算
prefixsum[5] = [5,5]+[1,4] = tree[5]+tree[4]
prefixsum[11] = [11,11]+[9,10]+[1,8] = tree[11]+tree[10]+tree[8]
class NumArray:def __init__(self, nums: List[int]):n = len(nums)self.nums = [0]*nself.tree = [0]*(n+1) #初始化当成nums[i]每个元素从0更新到nums[i]for i,x in enumerate(nums):self.update(i,x)def update(self, index: int, val: int) -> None:delta = val-self.nums[index]self.nums[index]=vali = index+1while i<len(self.tree):self.tree[i]+=deltai+=i&-i  # 下一个包含nums[index]的数字def sumRange(self, left: int, right: int) -> int:return self.prefixsum(right+1)-self.prefixsum(left)def prefixsum(self,i):ans = 0while i:ans+=self.tree[i]i-= i&-i # return ans 
# Your NumArray object will be instantiated and called as such:
# obj = NumArray(nums)
# obj.update(index,val)
# param_2 = obj.sumRange(left,right)

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

计算机网络 (62)移动通信的展望

一、技术发展趋势 6G技术的崛起 内生智能&#xff1a;6G将强调自适应网络架构&#xff0c;通过AI驱动的智能算法提升通信能力。例如&#xff0c;基于生成式AI的6G内生智能架构将成为重要研究方向&#xff0c;实现低延迟、高效率的智能通信。信息编码与调制技术&#xff1a;新型…

数据结构 前缀中缀后缀

目录 前言 一&#xff0c;前缀中缀后缀的基本概念 二&#xff0c;前缀与后缀表达式 三&#xff0c;使用栈实现后缀 四&#xff0c;由中缀到后缀 总结 前言 这里学习前缀中缀后缀为我们学习树和图做准备&#xff0c;这个主题主要是对于算术和逻辑表达式求值&#xff0c;这…

音视频多媒体编解码器基础-codec

如果要从事编解码多媒体的工作&#xff0c;需要准备哪些更为基础的内容&#xff0c;这里帮你总结完。 因为数据类型不同所以编解码算法不同&#xff0c;分为图像、视频和音频三大类&#xff1b;因为流程不同&#xff0c;可以分为编码和解码两部分&#xff1b;因为编码器实现不…

AI大模型开发原理篇-1:语言模型雏形之N-Gram模型

N-Gram模型概念 N-Gram模型是一种基于统计的语言模型&#xff0c;用于预测文本中某个词语的出现概率。它通过分析一个词语序列中前面N-1个词的出现频率来预测下一个词的出现。具体来说&#xff0c;N-Gram模型通过将文本切分为长度为N的词序列来进行建模。 注意&#xff1a;这…

Windows系统本地部署deepseek 更改目录

本地部署deepseek 无论是mac还是windows系统本地部署deepseek或者其他模型的命令和步骤是一样的。 可以看: 本地部署deepsek 无论是ollama还是部署LLM时候都默认是系统磁盘&#xff0c;对于Windows系统&#xff0c;我们一般不把应用放到系统盘&#xff08;C:&#xff09;而是…

知识库管理如何推动企业数字化转型与创新发展的深层次探索

内容概要 在当今数字化转型的大背景下&#xff0c;知识库管理日益显现出其作为企业创新发展的核心驱动力的潜力。这种管理方式不仅仅是对信息的存储与检索&#xff0c;更是一种赋能&#xff0c;以提升决策效率和员工创造力。企业能够通过系统地整合和管理各类知识资源&#xf…

商品列表及商品详情展示

前言 本文将展示一段结合 HTML、CSS 和 JavaScript 的代码&#xff0c;实现了一个简单的商品展示页面及商品详情&#xff0c;涵盖数据获取、渲染、搜索及排序等功能。 效果展示 点击不同的商品会展示对应的商品详情。 代码部分 代码总体实现 <!DOCTYPE html> <htm…

论文阅读:Realistic Noise Synthesis with Diffusion Models

这篇文章是 2025 AAAI 的一篇工作&#xff0c;主要介绍的是用扩散模型实现对真实噪声的仿真模拟 Abstract 深度去噪模型需要大量来自现实世界的训练数据&#xff0c;而获取这些数据颇具挑战性。当前的噪声合成技术难以准确模拟复杂的噪声分布。我们提出一种新颖的逼真噪声合成…

【汽车电子架构】AutoSAR从放弃到入门专栏导读

本文是汽车电子架构&#xff1a;AutoSAR从放弃到入门专栏的导读篇。文章延续专栏文章的一贯作风&#xff0c;从概念与定义入手&#xff0c;希望读者能对AutoSAR架构有一个整体的认识&#xff0c;然后对专栏涉及的文章进行分类与链接。本文首先从AutoSAR汽车软件架构的概念&…

毕业设计--具有车流量检测功能的智能交通灯设计

摘要&#xff1a; 随着21世纪机动车保有量的持续增加&#xff0c;城市交通拥堵已成为一个日益严重的问题。传统的固定绿灯时长方案导致了大量的时间浪费和交通拥堵。为解决这一问题&#xff0c;本文设计了一款智能交通灯系统&#xff0c;利用车流量检测功能和先进的算法实现了…

FPGA|使用quartus II通过AS下载POF固件

1、将开发板设置到AS下载挡位&#xff0c;或者把下载线插入到AS端口 2、打开quartus II&#xff0c;选择Tools→Programmer→ Mode选择Active Serial Programming 3、点击左侧Add file…&#xff0c;选择 .pof 文件 →start 4、勾选program和verify&#xff08;可选&#xff0…

适合超多氛围灯节点应用的新选择

文章目录 1.前言2.芯片简介2.1 高动态RGB照明2.2 灵活的通信模式2.3 精确的颜色控制2.4 高性能与可靠性2.5 易于集成与控制 3.硬件介绍3.1 方案框图3.2 通信模式3.3 器件选型 4.基础操作4.1 基础操作示例4.2 状态机4.3 启动行为4.4 诊断 5 颜色控制和温度稳定化5.1 颜色控制5.2…

MATLAB-Simulink并行仿真示例

一、概述 在进行simulink仿真的过程中常常遇到CPU利用率较低&#xff0c;仿真缓慢的情况&#xff0c;可以借助并行仿真改善这些问题&#xff0c;其核心思想是将参数扫描、蒙特卡洛分析或多工况验证等任务拆分成多个子任务&#xff0c;利用多核CPU或计算集群的并行计算能力&…

运算符重载(输出运算符<<) c++

我们来看下面这个Bug 报错1&#xff1a;打印整形&#xff08;int&#xff09;可以直接打印&#xff0c;打印字符&#xff08;char&#xff09;也可以直接打印&#xff0c;那是因为本身就已经给我们的内置类型准备好了一个输出运算符&#xff0c;可以直接用&#xff0c;但是我们…

【C++】类和对象(5)

目录 一、构造函数补充1、初始化列表 二、类型转换三、static成员四、友元1、友元函数2、友元类 五、内部类六、匿名对象 一、构造函数补充 对于之前讲解的构造函数&#xff0c;还有一些更深层次的内容要进行补充&#xff0c;接下来进行补充内容的讲解。 1、初始化列表 在我…

反向代理模块b

1 概念 1.1 反向代理概念 反向代理是指以代理服务器来接收客户端的请求&#xff0c;然后将请求转发给内部网络上的服务器&#xff0c;将从服务器上得到的结果返回给客户端&#xff0c;此时代理服务器对外表现为一个反向代理服务器。 对于客户端来说&#xff0c;反向代理就相当于…

Python - Quantstats量化投资策略绩效统计包 - 详解

使用Quantstats包做量化投资绩效统计的时候因为Pandas、Quantstats版本不匹配踩了一些坑&#xff1b;另外&#xff0c;Quantstats中的绩效统计指标非常全面&#xff0c;因此详细记录一下BUG修复方法、使用说明以及部分指标的内涵示意。 一、Quantstats安装及版本匹配问题 可以…

C#,入门教程(13)——字符(char)及字符串(string)的基础知识

上一篇&#xff1a; C#&#xff0c;入门教程(12)——数组及数组使用的基础知识https://blog.csdn.net/beijinghorn/article/details/123918227 字符串的使用与操作是必需掌握得滚瓜烂熟的编程技能之一&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; C#语言实…

主流的AEB标准有哪些?

目录 1、AEB的技术构成与工作原理 2、典型应用场景举例 3、AEB的功能分类 4、AEB系统性能评估的关键因素 5、全球AEB技术标准概览 5.1、联合国欧洲经济委员会&#xff08;UN ECE&#xff09; 5.2、美国NHTSA法规 5.3、中国标准 5.4、印度AIS 185 5.5、澳大利亚ADR法规…

Linux网络 | 网络层IP报文解析、认识网段划分与IP地址

前言&#xff1a;本节内容为网络层。 主要讲解IP协议报文字段以及分离有效载荷。 另外&#xff0c; 本节也会带领友友认识一下IP地址的划分。 那么现在废话不多说&#xff0c; 开始我们的学习吧&#xff01;&#xff01; ps&#xff1a;本节正式进入网络层喽&#xff0c; 友友们…