try ai
科普
编辑
分享
反馈
  • 增强树

增强树

SciencePedia玻尔百科
核心要点
  • 增强树通过在节点中添加额外数据来增强标准二叉搜索树,使其能够在对数时间内执行如排名和区间搜索等复杂查询。
  • 顺序统计树通过子树大小进行增强,能够像查找键一样高效地找到第 k 小的元素或一个元素的排名。
  • 区间树利用增强来存储最大端点,从而允许在调度和计算几何等领域快速查询重叠区间。
  • 增强原理为向数据结构添加新功能提供了一个通用方法,这需要在初始设置成本和长期查询效率之间进行权衡。

引言

二叉搜索树是计算机科学的基石,为存储和检索有序数据提供了一种优雅而高效的方式。它们能够以对数时间回答“这个项目是否存在?”的问题,这是一项了不起的算法效率成就。然而,它们的标准形式存在局限性。当面对关于数据结构更复杂的查询时,例如“第五小的项目是什么?”,“哪个预约与这个时间段重叠?”,或“给定范围内的值的总和是多少?”,它们就显得力不从心。用基本的二叉搜索树回答这些问题需要缓慢的、线性时间的操作,这抵消了树的主要优势。

本文探讨了一种克服这些局限性的强大技术:增强数据结构。它展示了我们如何通过在每个节点中存储少量经过精心选择的附加信息,为二叉搜索树赋予新的“超能力”,同时又不牺牲其核心效率。这种增强将一个简单的查找工具转变为一个能够回答复杂分析问题的动态引擎。

接下来的章节将引导您了解这个概念。首先,在“原理与机制”中,我们将剖析增强树的基本方法,使用顺序统计树和范围和查询作为核心示例,来理解如何存储、维护和使用这些额外信息。然后,在“应用与跨学科联系”中,我们将探讨这项技术的巨大现实世界影响,从通过区间树为物理引擎和调度系统中的碰撞检测提供动力,到解决计算金融中的优化问题。

原理与机制

我们拥有二叉搜索树这种奇妙的东西,它有点像一个为数字建立的、组织完美的归档系统。如果你正在寻找一个特定的数字,可以通过从根节点开始玩一个简单的“猜高低”游戏,以惊人的速度找到它。这非常有用,但如果我们想问更复杂的问题呢?如果我们不想问“数字42在哪里?”,而是想问“我的集合中第5小的数字是什么?”或“第10小和第20小之间的所有数字的总和是多少?”该怎么办?

标准的二叉搜索树对此束手无策。它知道顺序,但它不知道计数或排名。要找到第5小的元素,你必须进行一次中序遍历并数五步——这是一个效率低下的过程,尤其是在集合很大的情况下。另一种结构,最小堆,虽然能出色地告诉你第1小的元素(它总是在顶部!),但如果你问它第42小的元素,它就完全无能为力了。看来我们需要一种新的魔法。

这正是数据结构真正魅力开始闪耀的地方。我们不需要一个全新的发明;我们可以赋予我们现有的二叉搜索树一种超能力。我们可以增强它。

一棵会计数的树:顺序统计的力量

核心思想惊人地简单。如果树中的每个节点都对它所负责的树的那一部分有所了解呢?具体来说,如果每个节点都记录了它自己子树中(包括它自己)总共有多少个节点,会怎么样?我们称之为​​子树大小​​。

想象一个节点 xxx。我们用一个字段 x.sizex.sizex.size 来增强它。我们可以用一个简单的规则来定义它:x.size=(左子节点的大小)+(右子节点的大小)+1x.size = (\text{左子节点的大小}) + (\text{右子节点的大小}) + 1x.size=(左子节点的大小)+(右子节点的大小)+1。一个空位(一个 NIL 叶节点)的大小为 0。

这如何帮助我们找到第 kkk 小的元素呢?让我们试着从根节点开始寻找它。我们查看根节点的左子节点。假设它的大小是 LsizeL_{size}Lsize​。这意味着在整个集合中,恰好有 LsizeL_{size}Lsize​ 个元素小于根节点的键。因此,根节点本身在排序顺序中的位置是 Lsize+1L_{size} + 1Lsize​+1。

现在我们面临一个三路选择,一个我们可以在瞬间做出的决定:

  1. 如果我们的目标排名 kkk 正好等于 Lsize+1L_{size} + 1Lsize​+1,那么恭喜!根节点就是我们要找的元素。
  2. 如果 k≤Lsizek \le L_{size}k≤Lsize​,那么我们寻找的元素必定在左子树中。所以,我们只需移动到左子节点,并在那里继续寻找第 kkk 小的元素。
  3. 如果 k>Lsize+1k > L_{size} + 1k>Lsize​+1,那么元素必定在右子树中。但是我们已经跳过了左子树中的 LsizeL_{size}Lsize​ 个元素和根节点本身,总共 Lsize+1L_{size} + 1Lsize​+1 个元素。所以,在右子树中,我们不再寻找第 kkk 个元素,而是寻找第 (k−(Lsize+1))(k - (L_{size} + 1))(k−(Lsize​+1)) 个元素。

在每一步,我们都进行一次简单的比较,并向树下深入一层。如果树是平衡的——意味着它的高度与节点数的对数成正比,即 O(log⁡n)O(\log n)O(logn)——那么整个操作仅需 O(log⁡n)O(\log n)O(logn) 时间。我们已经将线性搜索转变为对数搜索,这就像将一次穿越全国的步行变成了一次短暂的飞机旅行。这种增强结构通常被称为​​顺序统计树​​(Order-Statistics Tree)。其逆操作,即查找给定键 xxx 的排名,也可以用类似的逻辑在 O(log⁡n)O(\log n)O(logn) 时间内完成。

力量的代价:维护魔法

这项令人难以置信的新能力并非完全免费。如果我们添加或删除一个元素,其所有祖先节点的子树大小都会改变。一次插入意味着我们必须沿路径回溯到根节点,递增路径上每个节点的 size 字段。

那么至关重要的平衡操作呢?为了保持我们的搜索时间为对数级,我们使用像红黑树或 AVL 树这样的自平衡树。这些树在插入或删除后会执行称为​​旋转​​的巧妙局部手术来维持平衡。一次旋转可能会改变父子关系,所以我们必须小心地更新我们的增强信息。值得庆幸的是,事实证明,在两个节点(比如 xxx 和 yyy)之间进行旋转后,它们新的子树大小仅使用其直接子节点的信息就可以重新计算。这个更新是一个常数时间,即 O(1)O(1)O(1) 的操作。因此,一次插入或删除的总成本仍然由树的高度主导,保持在简洁的 O(log⁡n)O(\log n)O(logn)。

这里有一个非常精妙的点。在你删除一个元素后,树可能正在进行一系列复杂的旋转和重新着色来维持其平衡。然而,从纯逻辑的角度来看,树中任何其他元素的排名会发生什么变化?假设你删除了一个键 ddd。对于任何其他键 k>dk > dk>d,其排名只是减少一。对于任何键 kdk dkd,其排名根本不变。排名的绝对变化要么是 0,要么是 1。树内部所有那些复杂的机制——修复路径、颜色翻转——都只是实现层为了确保它仍然能正确计算这个简单的、潜在的真理所做的工作。这是抽象属性与其具体实现的绝佳分离。

通用方法:不仅仅是计数

增强树的原理远比仅仅计数节点要通用得多。它是为数据结构赋予新能力的通用方法。基本思想是:

  1. 选择一个你想在每个节点中存储的附加信息。
  2. 弄清楚如何仅使用其子节点的信息来计算一个节点的这个信息。
  3. 确保在插入、删除和旋转期间能高效地维护这个信息。
  4. 使用这个新信息来回答以前困难或不可能的查询。

让我们用几种不同的“配料”来看看这个方法的实际应用。

​​超能力1:管理区间​​ 想象你正在构建一个日历应用程序。你有一组时间区间,比如 [1:00,2:30][1:00, 2:30][1:00,2:30] 或 [2:00,4:00][2:00, 4:00][2:00,4:00]。一个常见的问题是:“2:15 有什么会议?” 这被称为​​点刺查询​​。增强树如何提供帮助?

我们可以构建一个以这些区间的开始时间为键的树。这次的增强信息不是计数,而是子树中所有区间的​​最大端点​​。我们称这个值为 MMM。在任何节点,MMM 是其自身区间端点与其左右子节点 MMM 值中的最大值。

现在,当搜索包含点 xxx 的所有区间时,我们可以使用这个增强信息来极大地裁剪我们的搜索范围。假设我们位于一个节点,并考虑搜索其左子树。我们可以查看存储在左子节点中的 MMM 值。如果 xxx 大于这个值,这意味着 xxx 大于整个子树中任何区间的端点。它们中没有一个可能包含 xxx!所以,我们甚至不需要查看树的那个整个分支。这个简单的检查使我们能够避开树的巨大区域,使查询变得极其高效。

​​超能力2:计算范围和​​ 让我们再试一个。假设我们的树存储了以价格为键的产品。我们可能想问:“价格排名从第10贵到第20贵的产品总价值是多少?”

要回答这个问题,我们需要两个增强信息协同工作。我们需要之前的 subtree_size,这样我们才能按排名导航。我们还需要一个新的增强信息:​​子树和​​,即节点子树中所有键的总和。维护这个信息就像维护大小一样。

有了这两个信息,查询秩在 [i,j][i, j][i,j] 区间内的元素之和就变得很简单。我们可以将其表示为(秩直到 jjj 的元素之和)-(秩直到 i−1i-1i−1 的元素之和)。一个查找“秩直到 kkk 的元素之和”的函数之后便可以在 O(log⁡n)O(\log n)O(logn) 时间内导航树。它使用 subtree_size 来决定是向左还是向右走,并且在这样做时,它会利用它完全包含的子树的 subtree_sum 字段来智能地累加和。

可能性真的是无穷无尽的。我们甚至可以设计一种增强,来找到例如红黑树中第 kkk 小的黑色节点,这表明增强数据甚至可以与树的内部平衡属性相关联。

工程师的困境:何时进行增强?

拥有了所有这些惊人的能力,你可能会想,为什么我们不把所有东西都用增强树来做呢?答案在于一个实际的权衡。构建这些复杂的树有前期成本。为 nnn 个元素构建一个顺序统计树大约需要 O(nlog⁡n)O(n \log n)O(nlogn) 时间。

考虑一个替代方案。如果你只需要在一个简单的数组中找到第 kkk 小的元素一次,你可以使用像 ​​quickselect​​ 这样的算法,它平均在 O(n)O(n)O(n) 时间内完成工作。

这就提出了一个经典的工程难题。如果你有一个静态的数字列表,并且只需要回答一两个查询,那么 quickselect 的 O(n)O(n)O(n) 成本比构建增强树的 O(nlog⁡n)O(n \log n)O(nlogn) 成本要低。然而,如果你要回答许多查询(比如 qqq 个),情况就变了。使用树,在初始构建之后,qqq 个查询中的每一个都只需要 O(log⁡n)O(\log n)O(logn) 的成本。而重复使用 quickselect,你将支付 q×O(n)q \times O(n)q×O(n) 的成本。

这里存在一个​​盈亏平衡点​​。对于少量查询,简单的方法获胜。但随着查询数量的增加,总有一个时刻,构建增强树的一次性投资会得到回报,使其成为压倒性的优越选择。增强树是处理数据不断变化且需要反复提出复杂问题的动态场景的完美工具。它证明了通过准备好一个结构来使未来的工作变得轻松的力量——这是计算艺术中一个深刻且反复出现的主题。

应用与跨学科联系

我们花了一些时间来理解二叉搜索树的机制以及使其保持高效的巧妙平衡操作。它们是组织数据的宏伟结构,使我们能以惊人的速度查找、插入和删除项目。一个标准的搜索树回答的是“集合中是否有键‘42’?”这样的问题。但如果我们更有野心呢?如果我们的数据不仅仅是离散点的集合,而是代表着具有维度和结构的东西呢?如果我们想问诸如“4点钟有什么活动?”或“哪些公司在上次经济衰退期间是盈利的?”或“这个搜索结果在所有结果中的排名是多少?”之类的问题,该怎么办?

简单的二叉搜索树,尽管优雅,却对此沉默不语。要回答这些更深层次的问题,我们需要教我们的树一个新的技巧。我们需要增强它们。这不是一个小小的调整;这是一个飞跃,它将一个简单的归档系统转变为一个能够对世界进行推理的强大引擎。它完美地诠释了计算机科学中的一个深刻原理:通过向结构中添加少量精心选择的额外信息,我们可以极大地扩展其能力。

时间与空间的几何学:区间树

让我们从最自然、最普遍的数据形式之一开始:区间。一个区间就是一条线上的一个段——一段时间、一个价格范围、一段路程。想想你的日常日历。它就是一系列区间的集合:“团队会议,9:00-10:00”,“午餐,12:30-13:00”。如果我们将这些存储在一个以开始时间为键的标准树中,我们可以查出是否有会议在9:00开始,但我们无法轻易回答日历必须解决的最关键问题:“哪些会议与我提议的11:00-11:30咖啡时间重叠?”

为了解决这个问题,我们增强我们的树。在每个节点,除了区间本身,我们还存储一个额外信息:该节点为根的整个子树中所有区间的最高结束时间。把它想象成一个路标。当我们搜索重叠时,我们可能会到达树的一个分支,该分支代表所有下午才开始的会议。如果那个整个分支的路标告诉我们,其中最晚的结束时间是15:00,而我们的查询是针对16:00的,我们就可以绝对肯定地知道,我们不需要查看那个分支中的任何一个单独的会议。我们可以完全剪掉它。这个简单的增强使我们能够以对数效率锁定潜在的重叠。

这个单一而优美的思想在各种各样的领域中产生了令人惊讶的共鸣。

在视频游戏的​​物理引擎​​中,物体通常被封装在简单的“边界框”中。为了检测潜在的碰撞,引擎必须检查这些框是否重叠。如果我们将这些框投影到 xxx 轴和 yyy 轴上,我们会得到什么?是区间!一个增强树,通常称为区间树,可以管理这些投影,并告诉引擎忽略相距甚远的物体对,从而使其能够将计算能力集中在动作发生的地方。

在​​计算金融​​中,分析师可能希望识别出所有在特定时期内市盈率保持在“健康”范围(比如151515到202020)内的公司。每个这样的时期都是时间上的一个区间。要找到在特定市场崩盘期间所有健康的公司,分析师实质上是在一个庞大的数据库上执行区间重叠查询。

在​​计算几何​​中(这是计算机图形学和地理信息系统(GIS)的基础),一个经典问题是找到一组水平和垂直线段之间的所有交点。我们可以将水平线段视为 yyy 轴上的区间,并用一条线沿 xxx 轴扫描。当我们的扫描线遇到一个垂直线段时,它会提出一个查询:“当前活跃的水平线段中,哪些的 yyy 值在我的范围内?”这正是增强树为之构建的那种范围查询,它构成了一个高效的扫描线算法的核心。

这些结构的真正优雅之处在于它们的性能是“输出敏感的”。找到所有 kkk 个重叠区间所需的时间与区间总数不成正比,而是与 O(log⁡n+k)O(\log n + k)O(logn+k) 成正比。树的智能使其能够将时间花在答案上,而不是在草堆里。同样的原理也让我们能够构建动态日历系统,通过将繁忙时间表示为一组不相交的区间并查询它们之间的“空隙”,来高效地找到下一个可用的、特定时长的空闲时段。

计数与排名:知晓大小的力量

让我们换一个问题。我们不再问关于几何重叠的问题,而是问关于顺序和排名的问题。想象一下,你正在构建一个像grep这样的工具,用于在一个大文本文件中搜索一个词。工具发现“discovery”这个词出现在第 5,42,113,256,5, 42, 113, 256,5,42,113,256, 和 102410241024 行。你如何快速跳转到第3次出现的位置(第 113113113 行)?一个存储行号的标准BST可以告诉你第 113113113 行是否是匹配项,但若不进行缓慢的中序遍历,它无法告诉你其排名。

在这里,我们使用一种不同的增强。在树的每个节点,我们存储它的size:以该节点为根的子树中的节点总数(包括它自己)。现在,当我们站在任何一个节点时,我们能立即知道它在自己子树中的排名。其左子树中的节点数,我们称之为left_size,告诉我们有left_size个元素比当前元素小。因此,当前节点是其局部世界中第(left_size + 1)小的元素。

有了这些知识,在整个树中找到第 kkk 小的元素就变成了一次沿着单条路径的快速行走。在根节点,我们查看left_size。我们要找的排名是否小于left_size + 1?那么我们的目标一定在左子树。我们的排名是否更大?那么我们的目标在右子树,并且我们现在要在那里寻找第(kkk - left_size - 1)个元素。每一步都将搜索空间减半。这种“顺序统计树”赋予了我们排序数组的能力——按排名随机访问——但它是在一个支持快速插入和删除的动态结构上实现的。

从简单聚合到复杂的“Beats”

增强的原理是通用的。我们可以存储任何可以从节点的子节点信息中高效组合出来的信息。

考虑一个动态版本的​​部分背包问题​​,这是一个典型的优化难题。我们有一堆物品,每个物品都有重量和价值,我们想填充一个背包以最大化其总价值。最优策略是贪心:首先拿取价值重量比(密度)最高的物品。现在,如果物品在不断地被添加或移除呢?每次变动后都按密度重新排序会非常慢。

解决方案是一个增强树,节点按物品密度排序。我们增强每个节点,使其存储其子树中所有物品的总重量和总价值。要填充我们的背包,我们现在可以贪心地从最高密度到最低密度遍历树。如果整个右子树(包含更高密度的物品)能放入我们剩余的容量中,我们可以在一个逻辑步骤中全部拿走,将其总价值加到我们的解决方案中,并从我们的容量中减去其总重量,这一切都因为增强而能在常数时间内完成。我们只需要在必须部分拿取一个子树时才“深入”该子树。这将一个复杂的动态问题变成了一个快速的、有引导的搜索。这种使用树结构来维护前缀聚合的思想也是Fenwick树等数据结构背后的关键,它可以在经济模型中用于找到一个累积指标(如需求)超过某个容量的第一个时刻。

这条探究之路将我们带到了算法设计的前沿。如果我们需要应用一个非简单加减的更新该怎么办?考虑一个数组,我们想对一个范围应用一个“压缩”操作,将每个元素 xxx 映射到 ⌊x/2⌋\lfloor x/2 \rfloor⌊x/2⌋。这是一个非线性操作;一个范围的新和不是旧和的简单函数。一个标准的线段树将被迫更新每一个元素。然而,通过一个足够巧妙的增强——在这种情况下,存储范围内每个比特位置上置位比特的数量——我们可以懒惰地在树的整个段上计算这个复杂的非线性更新的效果。这种被称为“Segment Tree Beats”的先进技术表明,增强的力量只受限于我们寻找正确信息来存储的创造力。

一个统一的愿景

从安排我们的一天,到渲染一个虚拟世界,到搜索知识,再到优化一个供应链,一条共同的线索出现了。我们不断地面临着各种数据集合,其中项目之间的关系与项目本身同样重要。增强树是一个深刻而实用的解决方案的体现。它将一个健壮、有序的结构——平衡二叉树——与捕捉其内部子问题本质的摘要信息相结合。它告诉我们,通过理解我们想问的问题,我们可以丰富我们的数据结构来回答它们,不仅是正确地回答,而且是以惊人的效率和优雅来回答。这证明了最强大的工具往往是最简单的想法,但需要用远见和创造力来应用。