【问题标题】:Data structure for finding maximum of subarrays in better than O(log n) expected time用于在优于 O(log n) 预期时间内找到最大子数组的数据结构
【发布时间】:2016-10-19 21:28:13
【问题描述】:

给定一个值数组,如何构建一个数据结构,让您快速找到任何连续子数组的最大值?理想情况下,构建此结构的开销应该很小,并且该结构应该允许有效地追加和更改单个元素。

一个示例数组是[6, 2, 3, 7, 4, 5, 1, 0, 3]。一个请求可能是从索引 2 到 7(子数组 [3, 7, 5, 1, 0])中查找最大切片,这将导致 7

【问题讨论】:

  • 不幸的是,我的谷歌搜索被许多共享相似术语的基本问题所排挤。如果有人知道如何澄清或改进此问答的 Google 能力(或找到我未能找到的任何相关材料),请分享。

标签: arrays data-structures language-agnostic time-complexity


【解决方案1】:

n 为数组的长度,k 为切片的长度。

天真的,O(log k),方法

一个明显的解决方案是构建一棵树,重复给出最大值的成对汇总

1 8 4 5 4 0 1 5 6 9 1 7 0 4 0 9 0 7 0 4 5 7 4 3 4 6 3 8 2 4 · ·
 8   5   4   5   9   7   4   9   7   4   7   4   6   8   4   ·
   8       5       9       9       7       7       8       4
       8               9               7               8
               9                               8
                               9

这些摘要最多占用O(n) 空间,并且可以通过使用短索引有效地存储较低级别。例如,底层可以是位数组。追加和单一突变需要O(log n) 时间。如果需要,还有许多其他方面需要优化。

所选切片可以分成两个切片,在两个三角形之间的边界上拆分。在此示例中,对于给定的切片,我们将这样拆分:

                   |---------------------------------|
                6 9 1 7 0 4 0 9|0 7 0 4 5 7 4 3 4 6 3 8 2 4 · ·
                 9   7   4   9 | 7   4   7   4   6   8   4   ·
                   9       9   |   7       7       8       4
                       9       |       7               8
                               |               8

在每个三角形中,我们都对这些树中的 forest 感兴趣,它们最低限度地确定了我们实际关心的元素:

                   |---------------------------------|
                    1 7 0 4 0 9|0 7 0 4 5 7 4 3 4 6 3
                     7   4   9 | 7   4   7   4   6
                           9   |   7       7
                               |       7

请注意,在这种情况下,左侧有两棵树,右侧有三棵树。树的总数最多为O(log k),因为任何给定高度最多有两棵树。我们可以通过一点点数学找到分裂点

round_to = (start ^ end).bit_length() - 1
split_point = (end >> height) << height

请注意,Python 的 bit_length 可以通过 x86 架构上的 lzcnt 指令快速完成。相关树位于拆分的每一侧。相关子树的大小被编码在这些数字的残差位中:

lhs_residuals = split_point - start
rhs_residuals = end - split_point

bin(lhs_residuals)
# eg.     10010110
# sizes = 10000000
#            10000
#              100
#               10

很难遍历整数的最高有效位,但是如果您进行位交换(字节交换指令加上一些移位和掩码),则可以通过迭代来遍历最低有效位:

new_value = value & (value - 1)
lowest_set_bit = value ^ new_value
value = new_value

遍历左半部分和右半部分需要 O(log k) 预期时间,因为最多有 2log₂ k 树 - 每边一个位。

切线:处理O(1) 时间和O(n log n) 空间中的残差

O(log k)O(log n) 好,但仍然不是开创性的。先前尝试的一个有益效果是,每一侧的树都“连接”到一侧。他们的切片中只有n 范围,而不是任意切片的。您可以通过添加每个级别的累积最大值来利用它,如下所示:

1 8 4 5 4 0 1 5 6 9 1 7 0 4 0 9 0 7 0 4 5 7 4 3 4 6 3 8 2 4 · ·

- 8|- 5|- 4|- 5|- 9|- 7|- 4|- 9|- 7|- 4|- 7|- 4|- 6|- 8|- 4|- ·  left to right
8 -|5 -|4 -|5 -|9 -|7 -|4 -|9 -|7 -|4 -|7 -|4 -|6 -|8 -|4 -|· -  right to left

- - 8 8|- - 4 5|- - 9 9|- - 4 9|- - 7 7|- - 7 7|- - 6 8|- - · ·  left to right
8 8 - -|5 5 - -|9 9 - -|9 9 - -|7 7 - -|7 7 - -|8 8 - -|4 4 - -  right to left

- - - - 8 8 8 8|- - - - 9 9 9 9|- - - - 7 7 7 7|- - - - 8 8 · ·  left to right
8 8 5 5 - - - -|9 9 9 9 - - - -|7 7 7 7 - - - -|8 8 8 8 - - - -  right to left

- - - - - - - - 8 9 9 9 9 9 9 9|- - - - - - - - 7 7 7 8 8 8 · ·  left to right
9 9 9 9 9 9 9 9 - - - - - - - -|8 8 8 8 8 8 8 8 - - - - - - - -  right to left

- - - - - - - - - - - - - - - - 9 9 9 9 9 9 9 9 9 9 9 9 9 9 · ·  left to right
9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 - - - - - - - - - - - - - - - -  right to left

标记- 用于忽略那些必须与它们下面的级别相同的部分,不需要复制。在这种情况下,相关切片是

                   |---------------------------------|
                    1 7 0 4 0 9 0 7 0 4 5 7 4 3 4 6 3
                    ↓                               ↓
                9 9 9 9 - - - -|- - - - - - - - 7 7 7 8 8 8 · ·
                 right to left | left to right

和所需的最大值如所示。真正的最大值是这两个值中的最大值。

这显然占用了O(n log n) 内存,因为有log n 级别并且每个级别都需要一整行值(尽管它们可以是索引以节省空间)。然而,更新需要O(n) 时间,因为它们可能会传播 - 例如,向其添加 10 将使整个底部从右到左的行无效。突变显然同样低效。

O(1) 时间通过回答不同的问题

根据您需要的上下文,您可能会发现可以截断搜索深度。如果允许切片中相对于切片大小有一些余地,则此方法有效。由于切片在几何上收缩,尽管来自0:4294967295 的切片需要大量的 22 次迭代,但截断到固定数量的 11 次迭代会得到切片 0:4292870144 的最大值,相差 0.05%。这可能是可以接受的。

O(1) 预计利用概率的时间

舍入可能是可以接受的,但即使是这样,您仍在使用O(log n) 算法 - 只是使用较小的固定n。在随机分布的数据上可以做得更好。

考虑森林的一侧。当你遍历它时,你看到的数字的分数超过了你没有看到的几何分数。因此,您已经看到标准杆最大增加的概率。你可以利用它来发挥你的优势是有道理的。

再考虑这一半:

---------------------|
0 7 0 4 5 7 4 3 4 6 3 8 2 4 · ·
 7   4   7   4   6*  8   4   ·
   7       7       8*      4
       7*              8
               8

检查7*后,不要立即遍历6*。相反,检查其余的 all 中最小的父级,即8*。仅当此父级大于迄今为止的最大值时才向下遍历。如果不是,您可以停止迭代。只有更大了才需要继续往下遍历。碰巧这里的最大值在末尾之后,所以我们一直向下遍历,但你可以想象这是不寻常的。

至少一半的时间你只需要评估第一个三角形,至少一半的时间你只需要向下看一次,等等。这是一个几何序列,显示平均遍历成本是 两次遍历;如果你包括剩余的三角形在某些时候可能小于一半大小的事实,则更少。

在最坏的情况下呢?

最坏的情况发生在非随机树上。最病态的是排序后的数据:

---------------------|
0 1 2 3 4 5 6 7 8 9 a b c d e f
 1   3   5   7   9   b   d   f
   3       7       b       f
       7               f
               f

由于最大值始终位于您未见过的范围的片段中,无论您选择哪个切片。因此遍历总是O(log n)。不幸的是,排序数据在实践中很常见,这个算法在这里受到了伤害(这个属性与其他几个算法共享,比如快速排序)。不过,可以减轻伤害。

不会死于排序数据

如果每个节点都说明它是排序的还是反向排序的,那么在到达该节点时,您无需再进行任何遍历 - 您只需获取子数组中的第一个或最后一个元素。

---------------------|
0 1 2 3 4 5 6 7 8 9 a b c d e f
 →   →   →   →   →   →   →   →
   →       →       →       →
       →               →
               →

您可能会发现您的数据大部分是排序的,但有一些小的随机化,这会破坏该方案:

---------------------|
0 1 2 3 4 5 6 7 a 9 a b d 0 e f
 →   →   →   →   ←   →   ←   →
   →       →       b       f
       →               f
               f

因此,每个节点都可以在保持排序的同时向下移动的最大级别数,以及在哪个方向上。然后你跳过那么多迭代。一个例子:

---------------------|
0 1 2 3 4 5 6 7 a 9 a b d 0 e f

→1  →1  →1  →1  ←1  →1  ←1  →1
 0   3   5   7   a   b   d   f
  →2      →2      →1      →1
   3       7       b       f
      →3              →2
       7               f
              →3
               f

→n 表示如果您跳过n 级别,则节点将全部从左到右排序。顶部节点是→3,因为向下排序了三层:0 3 5 7 a b d f。方向很容易用一位编码。因此,大部分排序都得到了优雅的处理。

这很容易保持更新,因为每个节点都可以从其直接子节点计算其值。如果他们同意并且按照他们同意的相同方向排序,则最小距离并加一。否则重置为1 的距离并指向子元素的排序方向。最难的是遍历的逻辑,看起来有点挑剔。

仍然有可能产生需要一直遍历到底部的示例,但它们不应在非对抗性数据中频繁出现。

【讨论】:

    【解决方案2】:

    我偶然发现了这个问题的术语:

    范围最小查询

    不出所料,这是一个经过充分研究的问题,尽管它似乎很难搜索。 Wikipedia gives some solutions 与我的明显不同。

    O(1) 时间,O(n log n) 空间解决方案尤其比我的类似解决方案更有效,因为它允许在O(log n) 时间中追加,这可能就足够了,而不是我造成的可怕的O(n)

    其他的方法都是渐近体面的,最后的结果特别好。 O(log n) 时间,O(n) 空间解决方案在技术上比我的最终结果要弱,但 log n 永远不会很大,并且由于其内存的线性扫描,它在搜索时具有更好的常数因子。两种情况下的附加元素都被摊销了O(1),维基百科变体在足够小心的情况下做得更好。我希望将块大小设置为固定值并直接应用算法将是一个实际的胜利。在我的例子中,即使是 128 的块大小对于搜索来说也足够快,并且可以最大限度地减少追加开销和空间开销的常数因子。

    最终的恒定时间方法似乎是一个几乎没有实际用途的学术成果。

    【讨论】:

      猜你喜欢
      • 2012-09-08
      • 1970-01-01
      • 1970-01-01
      • 2011-08-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多