在 MoonBit 实现线段树(二)

embedded/2024/10/15 17:24:35/

引言

在上一篇文章当中我们讨论了最基础线段树的实现,但那棵线段树只能做到区间的查询(当然单点的修改与查询也是可以的),但做不到区间的修改(一个经典的应用是区间加法,即整个区间都加上某个值)。

在本节当中我们将基于上次的线段树继续加深抽象,引入 LazyTag 的概念来解决区间修改的问题,完成一棵功能基本完备的线段树。

怎么做到区间修改?

先设想如果我们在线段树上给一个区间都加上某个数会发生什么?或者换种说法,以最简单的办法来说,我们是如何完成它的?

在这里插入图片描述

以上节课的线段树为例,上面这张图中,我们对 [4, 7] 的区间都加上 1。这时候我们会发现这需要把涉及到这段区间的所有树上部分都重新构建维护一次,时间复杂度是 O(N Log N) 的,设想对整个区间都进行区间加法,那么所需的时间和建树是没有区别的,这个时间代价我们肯定是不能接受的。

那有没有更好的方法?当然有!可以使用 LazyTag!

在这里插入图片描述

设想我们在操作时,仅把 [4, 7] 区间的最少覆盖区间(就像查询需要的区间一样)打上一个 “+1” 的标记,并且根据这个区间的长度计算他应该有的值,然后合并上去,根据上节课 query 的复杂度类推,这个操作的复杂度应为 O(Log N) 的。

但有个问题,现在这种处理方法查询 [1, 7] 或者 [4, 7] 这些区间都没有问题,但如果我们要查询 [4, 6] 呢?容易发现对于区间 [4, 6],它的最小覆盖区间是 [4, 5] 与 [6, 6] 而不是 [4, 7],我们的 Tag 并没有对下面的节点生效。

下面我们就要用到 LazyTag 除了 Tag 外的另一个性质:Lazy。

在这里插入图片描述

我们规定在查询到某个节点时,如果当前节点上有一个加法的 Tag,就把它分发给下面的节点,下面的节点同样接收这个 Tag 并且根据自己的长度计算出自己应有的值。上图展示了在查询区间 [4, 6] 的结果时发生的 Tag 向下分发的操作。非常符合直觉的,叶子节点接收 Tag 之后可以直接转换为自身的改变而不需要再保留 Tag。

容易发现像这样的“懒惰下推 Tag”的方法可以保证每次修改的操作在 O(Log N) 的时间内完成,还可以保证查询时可以查询到正确的结果!

注:可能有些同学会疑问如果 Tag 重叠会怎么样,如果我们尝试一下就可以发现上文的这种加法 Tag 之间其实具有良好的合并性,不影响该节点总和的计算。

让我们来试试代码实现吧!

实现

说在前面

上节课在使用 let 结构 Node 时,我们是可以确定要解构的 enum 一定不是 Nil,但编译器是不能确定这一点的的,所以如果我们尝试这样去解构它:

let Node(x, y) = z

会发现编译器实际上给我们了一个警告,因为不影响运行并且语义简洁,所以笔者并没有删除这种写法。

但在本文发表之前 MoonBit 推出了新的 guard 语句,我们可以用 guard let 这种方法来更好的解决这种需求:

guard let Node(x, y) = z

基础定义

上节课的代码当中使用 enum 定义了线段树,但是每个 enum 当中的每个元素是用来干什么的其实没有名称标识,因为数据量比较小,对我们的心智负担影响不大,但目前我们需要添加 Tag 和 Length 属性存储,会显得匹配和定义的时候无法区分参数。

我们可以使用 enum 的 labeled-argument 写法来完成更好的定义:

enum Data {Data(~sum : Int, ~len : Int)
} derive(Show)enum LazyTag {NilTag(Int)
} derive(Show)enum Node {NilNode(~data : Data, ~tag : LazyTag, ~left : Node, ~right : Node)
} derive(Show)

这样我们就清晰地完成了对数据、LazyTag 和节点结构的定义,在下面初始化与模式匹配时将会更加清晰。

另外,我们把 Data 类型单独抽象出来,比上节课多了一个 len 属性,用来标记当前区间的长度,以配合 Tag 计算当前节点的值。

建树

我们依然像上一节一样在编写建树逻辑之前需要先考虑 Node 类型之间的加法,但本节中因为我们单独抽象了 Data,所以也要考虑他们之间的加法:

fn op_add(self : Data, v : Data) -> Data {match (self, v) {(Data(sum=a, len=len_a), Data(sum=b, len=len_b)) =>Data(sum=a + b, len=len_a + len_b)}
}fn op_add(self : Node, v : Node) -> Node {match (self, v) {(Node(data=l, ..), Node(data=r, ..)) =>Node(data=l + r, tag=Nil, left=self, right=v)(Node(_), Nil) => self(Nil, Node(_)) => v(Nil, Nil) => Nil}
}

可以发现这里暂时还没有考虑 LazyTag 的合并,而是认为他们加法的结果得到的节点的 LazyTag 均为 Nil,这是很好理解的,如果已经走到一个节点,那么它的父节点当然会是没有 LazyTag 的。

接下来就可以写出建树的代码,这与上节非常相似:

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

LazyTag 与区间修改的实现

我们把一个节点接受一个 LazyTag 的行为定义为 apply,容易发现其实真正的核心逻辑就在这里,当前接受上方 LazyTag 的节点身上不一定是否有 LazyTag,而如果有,又应该怎么合并?怎么根据 LazyTag 计算当前节点新的值?答案都在这个操作当中。

一个很好的实现方法是我们对 LazyTag 再单独定义一套加法运算来实现他们的合并,然后为 Node 类型编写一个 apply 函数来接收一个 LazyTag。

fn op_add(self : LazyTag, v : LazyTag) -> LazyTag {match (self, v) {(Tag(a), Tag(b)) => Tag(a + b)(Nil, t) | (t, Nil) => t}
}fn apply(self : Node, v : LazyTag) -> Node {match (self, v) {(Node(data=Data(sum=a, len=length), ~tag, ~left, ~right), Tag(v) as new_tag) =>Node(data=Data(sum=a + v * length, len=length),tag=tag + new_tag,~left,~right,)   (_, Nil) => self(Nil, _) => Nil}
}

这是我们这节课最核心的地方,根据当前区间长度和 LazyTag 的值计算出了当前节点的正确数值,这样我们就有了 LazyTag 的实现。

那么我们如何进行区间修改呢?

fn modify(self : Node,l : Int,r : Int,modify_l : Int,modify_r : Int,tag : LazyTag
) -> Node {if modify_l > r || l > modify_r {self} else if modify_l <= l && modify_r >= r {self.apply(tag)} else {guard let Node(~left, ~right, ..) = selfleft.apply(tag) + right.apply(tag)}
}

逻辑实际上与上节课编写的 query 大差不差,只是每个地方都让对应的节点 apply 了我们需要修改的值(作为 LazyTag)。

不过写到这里我们可以发现,这棵线段树就算加入了区间修改之后居然还是一个可持久化的,或者说 Immutable 的线段树!我们的 modify 函数将会返回最新的那棵线段树,并没有对原来的线段树作任何改变,而我们的递归与合并语义非常明显的体现了这一点。

这说明在一些 Immutable 的需求上上采用这类写法(ADT(enum)、递归)是非常优雅而且自然的。而且 MoonBit 语言存在垃圾回收机制 (GC),所以在无限递归的 ADT(enum) 当中不需要显式地用指针来指代一些关系,我们并不需要关心内存里面发生了什么。

很多对函数式编程语言不熟悉的读者可能使用 MoonBit 时没有太关注到这个问题,但其实我们一直从中受益,比如如果我们需要在 Rust 当中使用 ADT(enum) 来写一个 ConsList,我们往往需要:

enum List<T> {Cons(T, Box<List<T>>),Nil,
}

但在 MoonBit,我们只需要:

enum List[T] {Cons(T, List[T])Nil
}

GC is really interesting!

查询

查询部分只要记得需要下推 LazyTag 即可。

let empty_node : Node = Node(data=Data(sum=0, len=0),tag=Nil,left=Nil,right=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 {guard let Node(~tag, ~left, ~right, ..) = selflet mid = (l + r) >> 1left.apply(tag).query(l, mid, query_l, query_r) +right.apply(tag).query(mid + 1, r, query_l, query_r)}
}

总结

到这里我们就完成了一棵支持区间修改的,更加完美的线段树!

接下来,在最后一节课当中我们将会学习如何给当前这棵线段树再加入一个 “乘法操作”,以及探索一些 Immutable 线段树的应用场景。感兴趣的读者可以提前自行了解。


http://www.ppmy.cn/embedded/127978.html

相关文章

Python入门笔记(七)

文章目录 第十五章. 下载数据15.1 csv文件15.2 json文件 第十六章. 使用API16.1 requests 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。 点击跳转&#xff1a;人工智能从入门到精通教程 本文电子版获取…

《Oracle DB备份与恢复》开篇:一切从Oracle Incarnation开始

题记&#xff1a;从本篇开始&#xff0c;我将为大家介绍Oracle DB备份与恢复。备份恢复是DBA的核心工作&#xff0c;重在实操&#xff0c;多加练习&#xff0c;模拟各种DB或实例崩溃的场景。不同于一些博主一出场就讲如何备份恢复&#xff0c;我将从备份的源头原理开始介绍。本…

Unite Shanghai 2024 技术专场 | Unity 6及未来规划:Unity引擎和服务路线图

在 2024 年 7 月 24 日的 Unite Shanghai 2024 技术专场演讲中&#xff0c;Unity 高级技术产品经理 Jeff Riesenmy 带来演讲 Unity 6 and Beyond: A Roadmap of Unity Engine and Services。作为本次 Unite 首场专题演讲&#xff0c;他介绍了 Unity 引擎的最新进展及其配套的工…

django urlconf反向解析

Django 的 URLconf 反向解析是指通过 URL 的名称&#xff08;name 参数&#xff09;来生成 URL&#xff0c;而不是在代码中硬编码 URL 路径。这种方式更加灵活&#xff0c;方便在 URL 结构发生变化时&#xff0c;只需要修改 URL 模式&#xff0c;而不必修改代码中的所有路径引用…

golang获取当天最小的时间,以DateTime的string格式返回

推荐学习文档 golang应用级os框架&#xff0c;欢迎stargolang应用级os框架使用案例&#xff0c;欢迎star案例&#xff1a;基于golang开发的一款超有个性的旅游计划app经历golang实战大纲golang优秀开发常用开源库汇总想学习更多golang知识&#xff0c;这里有免费的golang学习笔…

02.06、回文链表

02.06、[简单] 回文链表 1、题目描述 编写一个函数&#xff0c;检查输入的链表是否是回文的。 2、解题思路&#xff1a; 快慢指针找中点&#xff1a; 利用快慢指针的技巧来找到链表的中间节点。慢指针 slow 每次移动一步&#xff0c;而快指针 fast 每次移动两步。这样&…

React中useEffect钩子

副作用&#xff1a;渲染以外的操作&#xff1a;像后端获取数据、操作DOM参数&#xff1a;副作用方法、依赖&#xff08;改变时重新执行&#xff09;调用时间&#xff1a;渲染JSX之后/依赖改变 useEffect 是 React 中的一个 Hook&#xff0c;用于在函数组件中执行副作用操作。副…

selenium的IDE插件进行录制和回放并导出为python/java脚本(10)

Selenium IDE&#xff1a;Selenium Suite下的开源Web自动化测试工具&#xff0c;是Firefox或者chrome的一个插件&#xff0c;具有记录和回放功能&#xff0c;无需编程即可创建测试用例&#xff0c;并且可以将用例直接导出为可用的python/java等编程语言的脚本。 我们以chrome浏…