【问题标题】:Data structure for trimming large values in an array?用于修剪数组中大值的数据结构?
【发布时间】:2012-01-23 17:23:33
【问题描述】:

为了以下目的,我需要一个数据结构。假设我有一个数组a。最初,所有元素都设置为零。每当我要在位置p 更新具有正值new_value 的元素之一时,如果位置p old_value 的原始值不为零并且大于new_value,那么我需要更新从位置p 一直到数组末尾的所有非零元素。这里的更新意味着用那个位置的旧值和new_value之间的较小的值重置值。

例如,数组是: [2, 0, 3, 0, 2, 1, 5, 0, 4, 0, 7]

假设位置2(从0 开始)的新值4 具有旧值3,我需要将数组更新为:

[2, 0, 3, 0, 2, 1, 4, 0, 4, 0, 4]

如果位置 2 的新值为 1,则结果数组为:

[2, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1]

是否有已知的数据结构可以有效地做到这一点?我需要很多这样的更新操作。

感谢您的建议。

【问题讨论】:

  • 数组中的值有多大?
  • @izomorphius:这有关系吗?一般可以是 LONG_MAX。
  • @QiangLi:两个问题。与需要更新的频率相比,您需要多久访问一次元素?并且:除此修剪操作之外,您还需要以任何方式更改数组吗?
  • @ErikP.:我需要像更新一样经常访问元素。不,我只需要以这种方式重置/更新元素。
  • @QiangLi 对于我想到的第一个解决方案来说确实很重要。但是,对于我的回答中描述的那个,它没有。我相信笛卡尔树就是您要寻找的。​​span>

标签: arrays algorithm data-structures


【解决方案1】:

我相信,您可以通过使用扩展树的修改,在每个元素访问或值更改的摊销 O(log n) 时间内完成这项工作。该方法背后的想法是双重的。首先,我们不是将数组存储为数组,而是将其存储为一对保存原始值的数组和一个展开树,其中每个节点的键是数组的索引。例如,给定一个包含七个元素的数组,设置可能如下所示:

Array: 3 1 4 2 5 9 3
Tree:             3
                /   \
               1.    5
              / \.  / \
             0.  2 4.  6

请注意,树将索引保存到数组中,而不是数组元素本身。如果我们想在特定索引处查找一个值,我们只需对索引进行展开树查找,然后返回给定位置的数组元素,这需要平均 O(log n) 时间。

您想要支持的更改所有未来值的操作我将称为“上限”操作,因为它为当前值之后的所有值设置了上限。考虑这一点的一种方法是,数组中的每个元素都有一个与之关联的上限(最初是无穷大),然后元素的真实值是其真实值和上限的最小值。诀窍在于,通过使用 splay 树,我们可以在摊销 O(log n) 时间内调整某个点处或之后的所有值的上限。为此,我们通过让每个节点存储一个值 c 来扩充展开树,该值表示从该元素向前施加的上限,然后根据需要对 c 进行适当的更改。

例如,假设我们想要从某个元素向前施加一个天花板。让我们假设这个元素已经在树的根部。在这种情况下,我们只需将其 c 值设置为 O(1) 时间内的新上限。从那时起,每当我们查找该元素处或之后的某个值时,当我们沿着树从根走到所讨论的元素时,我们可以记下天花板。更一般地,当我们进行查找时,每次我们跟随右子链接时,我们都会注意到该节点的 c 值。一旦我们找到有问题的元素,我们就知道该元素的上限,因为我们可以从我们跟随的右子节点的根路径上获取节点的最小上限。因此,为了在结构中查找元素,我们进行标准的展开树查找,跟踪我们所走的 c 值,然后输出 origins 数组值和 c 值的最小值。

但为了使这项工作发挥作用,我们的方法必须考虑到伸展树进行旋转的事实。换句话说,我们必须展示如何在旋转期间传播 c 值。假设我们想做这样的旋转:

    A.         B
   /.      ->.  \
  B.             A

在这种情况下,我们不会更改任何 c 值,因为在 A 之后查找的任何值仍将通过 A 节点。但是,如果我们做相反的旋转并将A拉到B之上,那么我们将A的c值设置为B的c值和A的c值的最小值,因为如果我们在执行旋转后下降到A的左子树,我们需要考虑因素考虑到 B 的上限。这意味着我们每次旋转执行 O(1) 工作,并且由于每次展开执行的摊销旋转次数为 O(log n),我们每次查找执行摊销 O(log n) 工作。

为了完成图片,更新任意天花板,我们将要改变天花板的节点展开到根,然后设置它的c值。

简而言之,我们有 O(log n) 查找 O(log n) 更改时间(摊销)。

这个想法基于 Sleator 和 Tarjan 的原始论文“自调整二叉搜索树”中对链接/切割树的讨论,该论文还介绍了伸展树。

希望这会有所帮助!

【讨论】:

    【解决方案2】:

    我最初的想法有点类似于templatetypedef's answer(顺便说一下,+1),但使用简单的静态二叉树而不是展开树。就像在那个答案中一样,“逻辑”数组L 由包含原始值的实际数组A 和强加上限值的二叉树T 表示。 (在这种情况下,上限值树的 shape 是静态的,因此我们不需要跟踪树中元素的索引:对应于节点的索引只是它的中序遍历索引。)树可以是任何形状,只要它具有最小的高度;也就是说,如果L 包含n 元素和2^(k-1) <= n < 2^k,那么树的高度应该是k。我建议一种布局,我们将元素2^(k-1) - 1 放在树的根部,使其左子树成为包含L[0 .. 2^(k-1) - 2]perfect 树,并为L[2^(k-1) .. n - 1] 递归定义其右子树(也就是说,它可能是空的)。例如,一个 12 元素树将具有如下条目:

                  7
           /-----/ \-----\
          3               11 
       /-/ \-\         /-/
      1       5       9   
     / \     / \     / \
    0   2   4   6   8   10
    

    (请注意,这些数字不是树的条目 - 它们只是表示树的条目对应于数组中的哪个位置。)这个描述还给出了在树中找到对应于条目的条目的算法i out of n: if i < 2^(k-1) - 1 then find the ith entry in the perfect left subtree, if i = 2^(k-1) - 1 then it's the root,否则从n - (2^(k-1) - 1)中找到i - (2^(k-1) - 1)th条目在右子树中递归。

    我们将所有树条目初始化为无穷大。当我们想对条目 i 及以后的条目施加 c 的上限时,我们会在树中找到第 ith 条目,如下所示:

    • 如果我们正在查看的节点 x 是第 ith 节点,或者我们需要下降到其 left 子树,我们将存储在节点 x 的值更新为c 的最小值和 x 的当前值。
    • 如果我们需要下降到x 子树并且存储在x 的当前值最多为c,我们不需要更新树 - 它是已经强制执行,所以我们可以停止。
    • 如果xith 节点,我们可以停止。

    这就是设置天花板的全部内容。请注意,A 本身永远不会更新。

    如果我们要查找Lith值,那么我们将一个局部变量c初始化为无穷大,并根据这些规则在树中找到ith条目:

    • 如果我们正在查看的节点 x 是第 ith 节点,或者我们需要下降到其 子树,则将 c 设置为其当前值的最小值以及存储在x 的值。
    • 如果xith 节点,我们可以停止。

    现在我们返回A[i]c 的最小值。

    这两个操作都是O(log n)(每个操作的实际时间,未摊销)。为了实现,请注意您可以使用数组来保存二叉树。

    【讨论】:

      【解决方案3】:

      我相信Cartesian tree 可以帮助您解决问题。与维基百科中描述的唯一区别是我建议您在每个节点中构建最大堆而不是最小堆(即将属性更改为每个节点的值不小于它的两个子节点)。 您需要转到当前节点的右子节点并沿着树向下更改所有值大于新值的元素。您可以证明通过这种方式您将检查的元素不超过 3*K,其中 K 是需要更改的实际元素数量。另一件好事是,通过执行操作,您在每个顶点中描述的堆属性仍将保留,因为您更改所有值大于新值。

      希望这个答案有帮助。

      【讨论】:

        猜你喜欢
        • 2020-05-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多