首页 > 编程语言 >编程实践|用 MoonBit 实现线段树(三)

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

时间:2024-12-20 10:31:03浏览次数:4  
标签:Node Tag Nil Int 线段 编程 MoonBit self

引言

在上一篇文章当中我们讨论了如何实现一棵支持区间查询、区间加法的 Immutable 线段树,并且使用了很多 MoonBit 语言当中的独特语法。

而作为“在 MoonBit 实现线段树”系列文章的最后一节,让我们来探讨一下如何实现一个同时支持区间乘法和区间加法的线段树,并且探索 Immutable 的函数式线段树在某些场景中的应用。

区间乘法

基于上一篇文章的线段树,如果我们在当时的需求基础上再加一个需求:实现对 [l, r] 区间内的元素乘以 v,我们应该怎么做呢?

在此之前我们已经学习了 LazyTag,易知其实新加入的操作只影响 Tag 的构成、Tag 与 Tag 的合并、Tag 与 Node 的合并三个部分,让我们来分别思考他们。

Tag 的构成

由于新加入了一个“乘法”的操作,我们将原本 Tag 的结构从 Tag(Int) 拓展为 Tag(Int, Int),并且使用 label 特性标明乘法 Tag 与加法 Tag,使其在使用时更加轻松。

enum LazyTag {
  Nil
  Tag(add~ : Int, mul~ : Int)
} derive(Show)

Tag 与 Tag 的合并

原本只有加法的 Tag 与 Tag 的合并是这样的:

fn op_add(self : LazyTag, v : LazyTag) -> LazyTag {
  match (self, v) {
    (Tag(a), Tag(b)) => Tag(a + b)
    (Nil, t) | (t, Nil) => t
  }
}

我们实际上利用了数学当中的“加法交换律”,即在操作只有加法的情况下,两个 Tag 的合并顺序和最终结果是没有关系的,因为他们所代表的操作均为加法,Tag 合并的结果也只是把他们对节点加上的值相加起来而已。

而一旦引入了“乘法操作”,根据数学定律,乘法的运算顺序优于加法,则其实 Tag 之间的合并次序会出现“先后”,因为乘法总是要比加法先一步计算,所以我们假设 op_add 函数的第一个参数为较老的 Tag,第二个参数为较新的 Tag。则其实我们注意到应该先将新 Tag 的乘法部分 apply 到老 Tag 的乘法/加法部分,再将新 Tag 的加法部分 apply 到老 Tag 的加法部分,代码如下:

fn op_add(self : LazyTag, v : LazyTag) -> LazyTag {
  match (self, v) {
    (Tag(add=adda, mul=mula), Tag(add=addb, mul=mulb)) =>
      Tag(mul=mula * mulb, add=adda * mulb + addb)
    (Nil, t) | (t, Nil) => t
  }
}

Node 与 Tag 的合并

按照上一节的总结,我们只需要根据“先计算乘法再计算加法”的顺序来把 Tag 的信息转移到 Node 上即可:即先对 sum 的值计算乘法再对其计算加法:

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

这样我们就完成了一棵支持“区间乘法”的线段树,再对其补全一下对应的 API(实际上就是对不同 Tag 修改方式的缩写),需要注意的一点就是在进行 add 的操作时,乘法 Tag 的值应设置为 1,以代表这个 Tag 的乘法部分不影响整体结果:

fn mul(
  self : Node,
  l : Int,
  r : Int,
  modify_l : Int,
  modify_r : Int,
  value : Int
) -> Node {
  modify(self, l, r, modify_l, modify_r, Tag(add=0, mul=value))
}

fn add(
  self : Node,
  l : Int,
  r : Int,
  modify_l : Int,
  modify_r : Int,
  value : Int
) -> Node {
  modify(self, l, r, modify_l, modify_r, Tag(add=value, mul=1))
}

Immutable 线段树的一些实际应用

上篇文章当中我们介绍过 MoonBit 当中的垃圾回收(GC)机制,而基于这样的内存回收机制,不需要手动精细地管理内存,Immutable 的线段树就可以做到下方这样的内存分布:即每次修改产生新版本时只新建到根的一条路径,而原本基底不变,这就可以支撑我们解决更多有意思的问题。

在这里插入图片描述

比如下方这个问题:

  • 给定一个由 n 个整数构成的序列 a,对于指定的闭区间 [l, r] 查询其区间内的第 k 小值。

一个非常简单的想法肯定是先排序之后取出第 k 小值即可,但这样每次处理的复杂度是 O(N Log N) 的(假设使用的排序算法为快速排序)。

而在这里我们可以引入一种叫做可持久化权值线段树的数据结构来更优雅地解决这个问题:

  • 以 Immutable 的方法建立一棵权值线段树(即覆盖整个值域,可以统计某个数字出现的次数的线段树)
  • 接下来遍历这个数列,向该权值线段树的对应位置增加统计值并保存该版本。如遍历到了值 5,则线段树上 5 的位置就应该加一,并且我们额外保存这个新成立的版本。
  • 此时我们容易发现,如果我们希望得到 [l, r] 中多少数字的统计信息,只需要先取出第 r 个版本的线段树进行统计,再减去第 l-1 个版本的线段树统计结果即可,而对于该结果,我们只需要在区间上作“选择左边或者右边”的分治就可以找到题设需求的“第k小值”。

更新操作

我们接下来根据第一篇文章中的“支持”求和的线段树进行更改,将其添加一个单点修改(为某个位置的值+1)操作:

fn update(self : Node, l : Int, r : Int, k : Int) -> Node {
  match self {
    Node(sum~, left~, right~) => {
      let mid = (l + r) >> 1
      if k <= mid {
        Node(sum=sum + 1, left=update(left, l, mid, k), right~)
      } else {
        Node(sum=sum + 1, left~, right=update(right, mid + 1, r, k))
      }
    }
    Nil => Nil
  }
}

一些工具函数

为了对接下来的流程进行简写,我们暂时定义了一些工具函数来解决需求:

fn unwrap(self : Node) -> Int {
  match self {
    Node(sum~, ..) => sum
    Nil => 0
  }
}

fn left_tree(self : Node) -> Node {
  match self {
    Node(left~, ..) => left
    Nil => Nil
  }
}
  
fn right_tree(self : Node) -> Node {
  match self {
    Node(right~, ..) => right
    Nil => Nil
  }
}

可持久化权值线段树上的查找

这个操作是整个数据结构实现当中最重要的部分,根据上面的论述,对于查询第 k 小值的操作,我们只要选出 l-1 与 r 两个版本,然后再另外维护一个权值上的二分 l=1 r=len,一步一步地在权值线段树上二分查询即可:

fn search(left : Node, right : Node, l : Int, r : Int, k : Int) -> Int {
  let mid = (l + r) >> 1
  let x = right.left_tree().unwrap() - left.left_tree().unwrap()
  if l == r {
    return l
  }
  if k <= x {
    search(left.left_tree(), right.left_tree(), l, mid, k)
  } else {
    search(left.right_tree(), right.right_tree(), mid + 1, r, k - x)
  }
}

fn find(versions : Array[Node], l : Int, r : Int, k : Int) -> Int {
  search(versions[l - 1], versions[r], 1, 5, k)
}

fn main {
  // 假设值域为 [1, 5]
  let trees : Array[Node] = []
  let mut tree = build([0, 0, 0, 0, 0][:])
  trees.push(tree)
  let arr = [1, 2, 3, 4, 5, 5, 4, 3, 2, 1]
  for x in arr {
    tree = tree.update(1, 5, x)
    trees.push(tree)
  }
  println(find(trees, 1, 7, 5))
}

总结

在这篇文章中我们首先完成了 Tag 结构更加复杂的、支持区间乘法的线段树,然后又探索了 Immutable 线段树的一个优秀的应用场景——使用可持久化权值线段树求静态区间最小值,并在此过程中充分地利用了 MoonBit 的 GC 优势与丰富的语言特性。

自此“用 MoonBit 实现线段树”系列文章完结,感兴趣的读者可以自行继续阅读并研究线段树的更高级知识,如矩阵乘法线段树、线段树分裂等。

可持久化线段树的例题与内存结构参考自 https://oiwiki.com/ds/persistent-seg/。

标签:Node,Tag,Nil,Int,线段,编程,MoonBit,self
From: https://blog.csdn.net/m0_74743788/article/details/144604359

相关文章

  • Visual Studio C++ 汇编 混合编程
    VisualStudioC++汇编混合编程实验要求请用汇编语言编写实现GCD递推公式的子程序,对入口和出口参数形式不做要求,但需要用C语言函数来获取输入、调用汇编递推子程序,并且用C语言显示子程序返回的结果。VisualStudio2020下载下载时勾选C++桌面开发选项。环境配置选择......
  • P4556 [Vani有约会] 雨天的尾巴 /【模板】线段树合并
    [Vani有约会]雨天的尾巴/【模板】线段树合并题目背景深绘里一直很讨厌雨天。灼热的天气穿透了前半个夏天,后来一场大雨和随之而来的洪水,浇灭了一切。虽然深绘里家乡的小村落对洪水有着顽固的抵抗力,但也倒了几座老房子,几棵老树被连根拔起,以及田地里的粮食被弄得一片狼藉。无......
  • 洛谷题单指南-线段树-Points
    原题链接:https://www.luogu.com.cn/problem/CF19D题意解读:坐标系支持集中操作:1.添加一个点(x,y),保证不会重复2.删除一个点(x,y)3.查询刚好比一个点(x,y)的x,y都大的点,优先看刚好比x大的位置,如果该位置有多个点,取y最小的一个,找到则输出点的坐标,找不到则输出-1。解题思路:首先,可......
  • Java并发编程(并发安全)
    并发编程中两个关键问题:线程之间如何通信(隐式进行,对程序员完全透明)以及如何同步线程之间的通信由JMM(java内存模型)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见,抽象来说共享变量存储在主内存,每个线程有一个私有的本地内存,里面存放了该线程读/写共享变量的副本就......
  • OpenCL 编程步骤 2. 获取设备
    clGetDeviceIDs查询支持OpenCL设备列表:cl_intclGetDeviceIDs(cl_platform_idplatform,cl_device_typedevice_type,cl_uintnum_entries,cl_device_id*devices,......
  • 2、C#基于.net framework的应用开发实战编程 - 设计(二、二) - 编程手把手系列文章
    二、设计;二.二、设计用户界面; 这个编程例子主要用的VisualStudio2022开发的,所以此文记录VS2022的UI界面设计过程。 1、 窗体;1) 此例子的窗体主要是便签窗体;主要是便签的内容保存。还有一个标题栏,用于简要显示该便签的名称。 2) 还......
  • 面向对象编程,类和对象
    类的关键词Class类一般申明在namespace中,枚举和struct一般也在namespace中申明类的申明语法(类前面可以加访问修饰符)class类名{特征——成员变量行为——成员方法保护特征——成员属性构造函数和析构函数索引器运算符重载静态成员......
  • HarmonyOS应用开发---DevEco CodeGenie编程AI辅助工具安装
    首款开发鸿蒙原生应用的AI辅助编程工具作为DevEcoStudio的AI辅助编程工具,DevEcoCodeGenie通过智能问答、代码补全/生成、万能卡片三大核心功能,打造高效智能开发体验。DevEcoCodeGenie的智能问答功能就像是开发者的智慧伙伴。无论是初涉鸿蒙原生应用开发的新手,还是经验丰富......
  • OpenCL 编程步骤 1. 获取平台
    参考OpenCL平台clGetPlatformIDs使用如下函数查询来获得系统平台列表:cl_intclGetPlatformIDs(cl_uintnum_entries,cl_platform_id*platforms,cl_uint*num_platforms)在OpenCL程序中,上述函数可以调用两次:......
  • 网络编程一>HTTP协议详解,<一文搞懂HTTP协议,抓包工具使用,HTTP协议报头>
    目录:  一.获取HTTP协议: 二.HTTP基本格式及格式内容: 三.HTTP请求"报头"详情(header):  一.获取HTTP协议:一.HTTP是什么HTTP(全称为"超文本传输协议")是⼀种应用非常广泛的应用层协议. 当我们在浏览器中输入⼀个"网址",此时浏览器就会给对应的服务......