@@ -22,15 +22,15 @@ collect: true
22
22
23
23
我们可以通过引入额外的缓存层级,以略微增加更新操作的开销为代价,进一步加快区间查询的速度。例如,我们可以将序列划分为 $\sqrt[ 3] {n}$ 个“大桶”,然后将每个大桶进一步细分为 $\sqrt[ 3] {n}$ 个“小桶”,每个小桶包含 $\sqrt[ 3] {n}$ 个值。每个桶的和都被缓存;现在每次更新需要修改三个值,而区间查询则在 $O(\sqrt[ 3] {n})$ 时间内完成。
24
24
25
- 以此类推,我们最终会得到一种基于二分治策略的区间和缓存方法,使得更新和区间查询操作的时间复杂度均为 $O(\lg n)$。具体而言,我们可以构建一棵平衡二叉树 ,其叶节点存储序列本身,而每个内部节点则存储其子节点的和。(这对于许多函数式程序员而言应是一个耳熟能详的概念;例如,手指树(finger trees)(Hinze & Paterson, 2006; Apfelmus, 2009)便采用了类似的缓存机制。)由此产生的数据结构通常被称为线段树,[ ^ 2 ] 大抵是因为每个内部节点最终都缓存了底层序列的一个(连续)段的和。
25
+ 以此类推,我们最终会得到一种基于二分治策略的区间和缓存方法,使得更新和区间查询操作的时间复杂度均为[ ^ 2 ] $O(\log n)$ 。具体而言,我们可以构建一棵完全二叉树 [ ^ 3 ] ,其叶节点存储序列本身,而每个内部节点则存储其子节点的和。(这对于许多函数式程序员而言应是一个耳熟能详的概念;例如,手指树(finger trees)(Hinze & Paterson, 2006; Apfelmus, 2009)便采用了类似的缓存机制。)由此产生的数据结构通常被称为线段树,[ ^ 4 ] 大抵是因为每个内部节点最终都缓存了底层序列的一个(连续)段的和。
26
26
27
27
<figure ><img src =" https://static.cambridge.org/binary/version/id/urn:cambridge.org:id:binary:20250116174732089-0720:S0956796824000169:S0956796824000169_fig2.png " width =" 80% " ><figcaption >图 2: 线段树</figcaption ></figure >
28
28
29
29
图 2 展示了一棵基于长度为 $n=16$ 的示例数组构建的线段树(为简化起见,我们假设 $n$ 是 2 的幂,尽管将其推广到非 2 的幂的情况亦不难)。树的每个叶节点对应数组中的一个元素;每个内部节点都带有一个灰色条块,显示其所代表的底层数组段。
30
30
31
31
我们来看如何利用线段树来实现前面所述的两种操作,并使其均能在对数时间内完成。
32
32
33
- - 要更新索引 $i$ 处的值,我们还需要更新包含该值的所有缓存区间和。这些恰好是沿着从索引 $i$ 处的叶节点到树根路径上的所有节点;这样的节点数量级为 $O(\lg n)$。图 3 阐释了图 2 中示例线段树的更新过程;更新索引 5 处的条目仅需修改根节点到被更新条目路径上的阴影节点。
33
+ - 要更新索引 $i$ 处的值,我们还需要更新包含该值的所有缓存区间和。这些恰好是沿着从索引 $i$ 处的叶节点到树根路径上的所有节点;这样的节点数量级为 $O(\log n)$。图 3 阐释了图 2 中示例线段树的更新过程;更新索引 5 处的条目仅需修改根节点到被更新条目路径上的阴影节点。
34
34
- 要执行区间查询,我们自顶向下遍历树,同时记录当前节点所覆盖的范围。
35
35
- 若当前节点的范围完全包含在查询范围内,则返回当前节点的值。
36
36
- 若当前节点的范围与查询范围不相交,则返回 0。
@@ -42,7 +42,7 @@ collect: true
42
42
43
43
<figure ><img src =" https://static.cambridge.org/binary/version/id/urn:cambridge.org:id:binary:20250116174732089-0720:S0956796824000169:S0956796824000169_fig4.png " width =" 80% " ><figcaption >图 4: 在线段树上进行区间查询</figcaption ></figure >
44
44
45
- 在这个小例子中,看似我们访问了相当大比例的节点,但一般而言,我们访问的节点数量不会超过大约 $4 \lg n$ 个。图 5 更清晰地展示了这一点。整棵树中最多只有一个蓝色节点可以拥有两个蓝色子节点,因此,树的每一层最多包含两个蓝色节点和两个非蓝色节点。我们实际上执行了两次二分查找,一次用于寻找查询区间的左端点,另一次用于寻找右端点。
45
+ 在这个小例子中,看似我们访问了相当大比例的节点,但一般而言,我们访问的节点数量不会超过大约 $4 \log n$ 个。图 5 更清晰地展示了这一点。整棵树中最多只有一个蓝色节点可以拥有两个蓝色子节点,因此,树的每一层最多包含两个蓝色节点和两个非蓝色节点。我们实际上执行了两次二分查找,一次用于寻找查询区间的左端点,另一次用于寻找右端点。
46
46
47
47
<figure ><img src =" https://static.cambridge.org/binary/version/id/urn:cambridge.org:id:binary:20250116174732089-0720:S0956796824000169:S0956796824000169_fig5.png " width =" 80% " ><figcaption >图 5: 在更大的线段树上进行区间查询</figcaption ></figure >
48
48
@@ -55,4 +55,6 @@ collect: true
55
55
我们的目标,并非为此已解决的问题编写优雅的函数式代码。相反,我们的目标在于运用一种函数式的领域特定语言来处理位串,并结合等式推理,从第一性原理出发,推导并阐释这段令人费解的命令式代码——展示函数式思维与等式推理之力,即便用于理解以其他非函数式语言编写之代码亦能奏效。在对线段树(第 2 节)建立更深入的直觉之后,我们将看到芬威克树如何能被视为线段树的一种变体(第 3 节)。随后,我们将绕道探讨二进制补码表示法,开发一个适宜的位操作 DSL,并解释`LSB`函数的实现(第 4 节)。借助于该 DSL,我们将推导出在芬威克树与标准二叉树之间来回转换的函数(第 5 节)。最终,我们将能够推导出在芬威克树内部移动的函数:先将其转换为二叉树索引,执行显而易见的操作以实现二叉树内的预期移动,然后再转换回来。通过等式推理将转换过程融合消除,最终将揭示隐藏其后的`LSB`函数,一如预期(第 6 节)。
56
56
57
57
[ ^ 1 ] : 值得注意的是,本文及后续部分均采用基于 1 的索引方式,即序列中的首个元素索引为 1。此选择之缘由,后文将予以阐明。
58
- [ ^ 2 ] : 此处术语的使用存在一些混淆。截至本文撰写之时,维基百科关于线段树(Wikipedia Contributors, 2024)的条目讨论的是一种用于计算几何的区间数据结构。然而,谷歌搜索“segment tree”的大部分结果都来自算法竞赛领域,其中它指的是本文所讨论的数据结构(例如,参见 Halim et al., 2020, Section 2.8 或 Ivanov, 2011b)。这两种数据结构基本上是不相关的。
58
+ [ ^ 2 ] : 本文的 $\log$ 表示以 2 为底的对数,原文使用 $\lg$。
59
+ [ ^ 3 ] : 原文是 * balanced binary tree* ,翻译为平衡二叉树或有歧义,此处使用完全二叉树以避免歧义。
60
+ [ ^ 4 ] : 此处术语的使用存在一些混淆。截至本文撰写之时,维基百科关于线段树(Wikipedia Contributors, 2024)的条目讨论的是一种用于计算几何的区间数据结构。然而,谷歌搜索“segment tree”的大部分结果都来自算法竞赛领域,其中它指的是本文所讨论的数据结构(例如,参见 Halim et al., 2020, Section 2.8 或 Ivanov, 2011b)。这两种数据结构基本上是不相关的。
0 commit comments