编程实践|用 MoonBit 实现线段树(一)

引言

线段树(Segment Tree)是一种常见的数据结构,用于解决一些线性区间的修改、查询问题,比如对于问题:

  • 给出一个长度已知的、有初值的数字数组,接下来要进行许多区间加法操作(将一个区间的数值都加上某个值)和区间求和操作(求该区间数值的和并输出)

如果该问题使用正常的数组方式来遍历求解,假设该数组长度为 N,每次修改和查询的操作耗时是 O(N) 的;但线段树经过 O(N log N) 的构建之后,可以对上述两个操作做到 O(log N) 的优秀复杂度,足以体现其在区间问题上的重要性。

当然上面的例子只是线段树可以解决的一个简单问题,它可以做到的更复杂、更有趣的事情还有很多。在接下来的几篇文章当中我们将会学习使用线段树的概念以及如何使用 MoonBit 实现它,最终我们将一步步实现一棵支持区间加法与乘法、并可以查询区间和、拥有不可变特性的线段树。

本节我们将学习线段树的基本原理以及如何使用 MoonBit 编写一棵最基本的支持单点修改、查询的线段树。

线段树是什么?

本节是纯粹的概念、理论内容,如果读者已经了解并且熟悉线段树的构成与其原理,可以直接阅读下一节内容。

就像引言当中所说的,线段树可以解决一类区间问题,但他长什么样子,能做到如此优秀复杂度的原理又是什么呢?

我们以下图一个线性的数字序列为例,如果我们希望以它建立一棵线段树,那么它将会长这个样子:
在这里插入图片描述

可以看到我们把一个线性序列的区间层层分而治之,每次分割为两个对等(如果是奇数则一边多一个)的两个区间(区间范围下标在图示中),最终直到分割为长度为一的区间,并且在此过程中计算了其中每个区间元素的加和(在括号中),这样就从一个线性序列创建了一棵支持查询区间和线段树。

那么在查询区间和的时候,它如何工作呢?我们以查询区间 1-6 的和为例:

在这里插入图片描述

可以发现图中的标红部分加起来就等于区间 1-6 的区间和,而我们并没有统计到所有元素,只是选取了最少的区间来频出我们需要求解的区间,并且把我们要求的结果(此处为和)合并起来即可得到最终解。而我们只需要从上到下遍历这棵线段树来判断区间之间的交集/包含关系即可选择到符合条件的区间。

具体来说:

  • 首先询问区间 1-7 与 1-6 的关系,显然后者为前者的子集,当前 1-7 的数据不能用于统计,因此继续向下遍历两个子节点。
  • 再询问 1-3 与 1-6 的关系,前者为后者的子集,可以作为解的一部分,统计入当前结果中。
  • 接下来询问 4-7 与 1-6 的关系,二者有交集,因此要继续向下遍历两个子节点。
  • 然后询问 4-5 与 1-6 的关系,此处与第三条一致。
  • 以此类推…

根据二进制分解的知识,我们对任何长度为 N 的区间最多只会求解 Log N 个区间,因此复杂度是可以保证的。

这里仅聊到查询操作,关于线段树上的修改操作的原理和实现我们将会在下一节详细说明。

实现

基础定义

我们采用一个非常经典的方法来表达线段树:

enum Node {NilNode(Int, Node, Node)
} derive(Show)

其中 Nil 代表空树,而一个 Node 则包含一个它所储存的数据(为 Int 类型)和左右两个节点。

此外,我们还为他 derive 了 Show Trait,我们在遇到问题时可以直接输出这棵树来调试,这样非常直观且方便。

建树

建树是指将一个线性序列抽象为线段树的过程,一般将其称为 build

作为前置,我们应该根据需求为 Node 类型编写一个 op_add 的重载来配合下面建树的过程:

fn op_add(self : Node, v : Node) -> Node {match (self, v) {(Node(left, _, _), Node(right, _, _)) => Node(left + right, self, v)(Node(_), Nil) => self(Nil, Node(_)) => v(Nil, Nil) => Nil}
}

定义这一运算之后就可以轻松的向上合并两个 Node 节点,并在此过程中维护区间的和,为我们建树打下了基础,在有些线段树的叙述当中,这个过程也叫做 pushup

我们可以用 MoonBit 的 ArrayView 特性(某些语言当中也叫做 slice)作为参数来低成本的取出一个线性结构的一段进行递归建树,这个过程是 O(N Log N) 的:

fn build(data : ArrayView[Int]) -> Node {if data.length() == 1 {Node(data[0], Nil, Nil)} else {let mid = (data.length() + 1) >> 1build(data[0:mid]) + build(data[mid:])}
}

分析一下这段代码:

  • 首先如果当前长度已经为 1,就证明该区间不再需要细分,直接返回左右分支为空的叶子节点。
  • 否则就证明该区间还可被分割,则求其中间值将其分割为两个区间分而治之的建树再通过 Node 之间的加法合并。

这段代码是非常简洁、可读性非常高的,而且对优化非常友好,可以作为后续其他数据结构的范式学习。

让我们来建立一棵树并输出看看:

fn main {let tree = build([1, 2, 3, 4, 5][:])println(tree)
}

运行后的输出是:

Node(15, Node(6, Node(3, Node(1, Nil, Nil), Node(2, Nil, Nil)), Node(3, Nil, Nil)), Node(9, Node(4, Nil, Nil), Node(5, Nil, Nil)))

漂亮,我们已经成功完成了建树的过程!

查询

接下来我们要编写查询,因为这棵线段树的节点向上合并时维护的是区间和,因此我们可以编写一个 query 函数来查询它:

let empty_node : Node = Node(0, Nil, Nil)fn query(self : Node, l : Int, r : Int, query_l : Int, query_r : Int) -> Node {if query_l > r || l > query_r {empty_node} else if query_l <= l && query_r >= r {self} else {let Node(_, left, right) = selflet mid = (l + r) >> 1left.query(l, mid, query_l, query_r) +right.query(mid + 1, r, query_l, query_r)}
}

首先,lr是当前函数中已经查询到的区间,query_lquery_r是需要查询的区间,让我们来尝试解析一下这段实现:

  • 如果需查询的区间和当前的区间状态为互不相交,则对解没有贡献,我们定义了一个 empty_node 来表示 0 贡献节点,将其返回则为无贡献。
  • 如果当前区间就是需查询区间的子集,那么实际上对解的贡献就是它自己,直接返回它即可。
  • 如果当前区间和需要查询的区间存在交集关系,那么需要继续向下搜索来确定准确的覆盖,因此继续求出中间值向下搜索并且合并两边的 Node 结果。
Questions and Anwsers:
  • Q: 为什么要用 Node 作为返回值,我用相同的逻辑也可以直接把 Node 当中的值给解构出来相加呀?
  • A: 首先,我们为 Node已经编写了加和运算,不妨考虑一种情况,我们不止要维护区间和,而是要同时维护区间和还有区间最小值,这时候我们只需要更改 Nodeop_add 逻辑来维护最小值即可,而 query函数和我们要维护的数据没有关系,它最终返回的是一个 Node,它可以求出所有信息!所以不妨就让我们使用 Node
  • Q: 你说的这种情况 empty_node 是不是也要改变?
  • A: 对,empty_node 是用来保证它和任何其他 Node 相加都不会产生改变的元素,是一个零贡献的 Node,类比在维护区间和的时候零贡献是 0,那么其实对于维护最小值来说你的值是当前可以取到的最大值,那就是零贡献的,这个过程处理的其实很灵活!

让我们来测试一下这个查询过程:

fn main {let tree = build([1, 2, 3, 4, 5][:])let sum = match tree.query(1, 5, 1, 3) {Node(sum, _, _) => sum_ => panic()}println(sum)
}

输出是:

6			

太好了,我们得到了正确的输出!

代码

完整代码见下

enum Node {NilNode(Int, Node, Node)
} derive(Show)let empty_node : Node = Node(0, Nil, Nil)fn op_add(self : Node, v : Node) -> Node {match (self, v) {(Node(left, _, _), Node(right, _, _)) => Node(left + right, self, v)(Node(_), Nil) => self(Nil, Node(_)) => v(Nil, Nil) => Nil}
}fn build(data : ArrayView[Int]) -> Node {if data.length() == 1 {Node(data[0], Nil, Nil)} else {let mid = (data.length() + 1) >> 1build(data[0:mid]) + build(data[mid:])}
}fn query(self : Node, l : Int, r : Int, query_l : Int, query_r : Int) -> Node {if query_l > r || l > query_r {empty_node} else if query_l <= l && query_r >= r {self} else {let Node(_, left, right) = selflet mid = (l + r) >> 1left.query(l, mid, query_l, query_r) +right.query(mid + 1, r, query_l, query_r)}
}fn main {let tree = build([1, 2, 3, 4, 5][:])let sum = match tree.query(1, 5, 1, 3) {Node(sum, _, _) => sum_ => panic()}println(sum)
}

总结

今天我们学习了如何编写一棵简单的线段树的构建和查询操作的编写,下一节课我们将会学习更加复杂的线段树的原理和实现,感兴趣的读者可以在阅读文章之后自行实现下面内容来巩固知识和拓展更多内容:

  • 尝试实现一个可以维护多个信息(如区间和、区间最大值最小值)的线段树。
  • 自行了解如何实现线段树的单点查询/修改操作并实现。
  • 自行了解线段树的区间修改操作以及 LazyTag 的相关知识。

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

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

相关文章

线性dp 总结详解

就是感觉之前 dp 的 blog 太乱了整理一下。 LIS(最长上升子序列) 例题 给定一个整数序列&#xff0c;找到它的所有严格递增子序列中最长的序列&#xff0c;输出其长度。 思路 拿到题目&#xff0c;大家第一时间想到的应该是的暴力(dp)做法&#xff1a; #include <bits/s…

锐尔15注册机 锐尔文档扫描影像处理系统15功能介绍

锐尔文档扫描影像处理系统是一款全中文操作界面的文件、档案扫描及影像优化处理软件&#xff0c;是目前国内档案数字化行业里专业且优秀的影像优化处理软件。 无论是从纸质文件制作高质量的影像文件&#xff0c;或是检查已经制作好的影像文件&#xff0c;锐尔文档扫描影像处理…

LangChain 和 Elasticsearch 加速构建 AI 检索代理

作者&#xff1a;来自 Elastic Joe McElroy, Aditya Tripathi, Serena Chou Elastic 和 LangChain 很高兴地宣布发布新的 LangGraph 检索代理模板&#xff0c;旨在简化需要代理使用 Elasticsearch 进行代理检索的生成式人工智能 (GenAI) 代理应用程序的开发。此模板预先配置为使…

C++第十一节课 new和delete

一、new和delete操作自定义类型 new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间还会调用构造函数和析构函数&#xff08;new会自动调用构造函数&#xff1b;delete会调用析构函数&#xff09; class A { public:A(int a 0): _a(a){cout <&l…

使用express或koa或nginx部署history路由模式的单页面应用

使用hash模式会有#&#xff0c;影响美观&#xff0c;所以使用history模式会是个更好的选择。 前端项目打包上线部署&#xff0c;可以使用下面的方式部署history模式的项目&#xff0c;下面以 jyH5 为例 expressjs部署 express脚手架搭建的app.js中添加如下代码&#xff1a; …

Superset二次开发之优化Mixed Chart 混合图(柱状图+折线)

背景 基于Mixed Chart(柱状图+折线)作图,显示 某维度A Top10 + 其他 数据,接口返回了值为 undefined 的某维度A 数据,前端渲染成 某维度A 值为 0 此图表存在的问题: 图表控件编辑页面,即便数据集正常查询出 Top10 + ‘其他’ 数据,但是堆积图表渲染时,返回了 值为 0…

【网络通信基础与实践第四讲】用户数据报协议UDP和传输控制协议TCP

一、UDP的主要特点 1、UDP是无连接的&#xff0c;减少了开销和发送数据之前的时延 2、UDP使用尽最大努力交付&#xff0c;但是不保证可靠交付 3、UDP是面向报文的。从应用层到运输层再到IP层都只是添加一个相应的首部即可 4、UDP没有拥塞机制&#xff0c;源主机以恒定的速率…

Zookeeper安装使用教程

# 安装 官网下载安装包 #配置文件 端口默认8080&#xff0c;可能需要更改一下 #启动 cd /Users/lisongsong/software/apache-zookeeper-3.7.2-bin/bin ./zkServer.sh start #查看运行状态 ./zkServer.sh status #停止 ./zkServer.sh stop #启动客户端 ./zkCli.sh ls /

Linux ubuntu debian系统安装UFW防火墙图形化工具GUFW

GUFW是UFW的图形化前端&#xff0c;可以通过以下命令安装&#xff1a; sudo apt install gufw安装成功后&#xff0c;可以通过应用程序菜单启动GUFW&#xff0c;在图形界面中&#xff0c;可以方便地添加、修改和删除规则&#xff0c;查看状态和日志。

java之杨辉三角问题

给定一个非负整数 numRows&#xff0c;生成「杨辉三角」的前 numRows 行。 在「杨辉三角」中&#xff0c;每个数是它左上方和右上方的数的和。 如何实现呢&#xff1f; 思路&#xff1a;首先&#xff0c;我们可以将杨辉三角视作i行j列的二维数组。除了第一行和第二行之外&am…

IPD流程体系:IPD在硬件产品开发中的应用

目录 1、内容简介 2、开发各阶段介绍 3、PVT阶段 4、资源群更新 作者简介 1、内容简介 在硬件类相关产品的开发过程中&#xff0c; 每个阶段的工作都是需要按照一定的流程、规范和标准去进行的。 整体还是相对瀑布化的流程&#xff0c; 每个阶段的输入、输出、准入、准…

C++初阶学习——探索STL奥秘——反向迭代器

适配器模式是 STL 中的重要组成部分&#xff0c;除了容器适配器外&#xff0c;还有 选代器适配器&#xff0c;借助 选代器适配器 &#xff0c;可以轻松将各种容器中的普通迭代器转变为反向迭代器&#xff0c;这正是适配器的核心思想 注:库中的反向迭代器在设计时&#xff0c;为…

【HTTP】请求“报头”(Host、Content-Length/Content-Type、User-Agent(简称 UA))

Host 表示服务器主机的地址和端口号 URL 里面不是已经有 Host 了吗&#xff0c;为什么还要写一次&#xff1f; 这里的 Host 和 URL 中的 IP 地址、端口什么的&#xff0c;绝大部分情况下是一样的&#xff0c;少数情况下可能不同当前我们经过某个代理进行转发。过程中&#xf…

С++第十三节课 string初体验

一、string类的相关函数 string实际上也就是一个管理字符的顺序表&#xff01; 如果我们需要遍历一个字符串&#xff0c;怎么实现&#xff1f; 我们可以通过下标访问操作符 size实现字符串的遍历&#xff01; int main() {string s1("hello world");// 遍历一个字…

不可思议的效率飞跃:RPA如何重塑你的工作流程,释放人力潜能!

RPA简介 机器人流程自动化&#xff08;Robotic Process Automation&#xff0c;简称RPA&#xff09;是一种模拟人类用户操作的软件技术&#xff0c;它通过自动化执行重复性、规律性强的任务来提高工作效率和准确性。RPA软件机器人可以模拟鼠标点击、键盘输入、数据复制粘贴等操…

anaconda的windows新手安装及配置教程(适用于物联网工程、计算机专业)

第一步:点击免费下载 点击我直达anaconda官网">——>点击我直达anaconda官网 第二步:跳过注册 第三步:下载windows版本 第四步:安装步骤 1.Next (下一步) 2.I Agree (我同意) 3.默认即可,下一步 4.安装地址可以选到D盘,如果没有默认也行,只是一个…

OpenAI GPT o1技术报告阅读(4)- 填字游戏推理

✨继续阅读报告&#xff1a;使用大模型来学习推理(Reason) 原文链接&#xff1a;https://openai.com/index/learning-to-reason-with-llms/ 这次我们继续看一个填字游戏的案例。 我们先看下问题&#xff1a; 解决以下填字游戏&#xff1a; Across&#xff08;横向&#xff09…

推荐2024年好用的4款日语翻译工具

日语在学习研究&#xff0c;商务合作&#xff0c;旅游文化交流等多个领域还是占有着一个比较重要的作用&#xff0c;将中日两种语言进行准确地翻译能够这些活动更加高效有益地进行和发展。因此好的翻译工具便尤为重要&#xff0c;今天我也给大家挑选了几款优秀地日语翻译工具。…

可视化工具箱-Visualization Toolkit(VTK)

一、Visualization Toolkit&#xff08;VTK&#xff09;简概 可视化工具箱&#xff08;VTK&#xff09;&#xff0c;是一个用于3D计算机图形、图像处理和科学可视化的开源软件系统&#xff0c;其包含C类库和Tcl/Tk、Java与python的解释型接口层。VTK支持各种可视化算法&#xf…

电机设计及电机仿真APP系列之—轴向磁通电机仿真APP

电机的各种工作状态和参数变化。用户可通过调整仿真参数&#xff0c;快速得到电机的响应和性能参数&#xff0c;从而进行针对性的优化和改进。借助仿真APP&#xff0c;可大大减少电机设计迭代次数和成本&#xff0c;提高测试效率和准确性。 小编整理了10款不同类型的电机仿真A…