【问题标题】:Kth minimum in a Range范围内的第 K 个最小值
【发布时间】:2014-03-16 13:40:32
【问题描述】:

给定一个整数数组和一些查询操作。
查询操作有两种类型
1.将第i个索引的值更新为x。
2.给定2个整数在该范围内找到第k个最小值。(例如,如果2个整数是i和j,我们必须找出i和j之间的第k个最小值)。
我可以使用段树找到 Range 最小查询,但对于第 k 个最小值却不能这样做。 谁能帮帮我?

【问题讨论】:

  • 您期望的复杂度是多少? O(logN)? O(logN+k)? O(klogN)?
  • 快速选择算法不够快?
  • 这个问题的k是常数吗?或者查询包含 3 个名为 i、j 和 k 的整数?

标签: algorithm data-structures tree


【解决方案1】:

这里是每个查询的 O(polylog n) 解决方案,它实际上并不假定常量 k,因此 k 可以在查询之间变化。主要思想是使用段树,其中每个节点表示数组索引的间隔,并包含表示的数组段中值的多重集(平衡二叉搜索树)。更新操作非常简单:

  1. 从叶子(您正在更新的数组索引)向上走行段树。您将遇到表示包含更新索引的数组索引间隔的所有节点。在每个节点,从多重集中删除旧值并将新值插入多重集中。复杂性:O(log^2 n)
  2. 更新数组本身。

我们注意到每个数组元素都在O(log n) 多重集中,因此总空间使用量为O(n log n)。通过多集的线性时间合并,我们也可以在O(n log n) 中构建初始段树(每个级别都有O(n) 工作)。

查询呢?我们得到一个范围[i, j] 和一个等级k,并希望在a[i..j] 中找到第k 个最小的元素。我们如何做到这一点?

  1. 使用标准段树查询过程查找查询范围的不相交覆盖。我们得到O(log n) 不相交的节点,其多重集的并集恰好是查询范围内的值的多重集。让我们将这些多重集称为s_1, ..., s_m(与m <= ceil(log_2 n))。找到s_i 需要O(log n) 时间。
  2. s_1, ..., s_m 的并集执行select(k) 查询。见下文。

那么选择算法是如何工作的呢?有一种非常简单的算法可以做到这一点。

我们已经给定了s_1, ..., s_nk,并希望在a 中找到最小的x,这样s_1.rank(x) + ... + s_m.rank(x) >= k - 1,其中rank 返回小于x 的元素数BBST(如果我们存储子树大小,这可以在O(log n) 中实现)。 让我们使用二分查找来查找x!我们遍历根的 BBST,进行几个排名查询并检查它们的总和是否大于或等于 k。这是x 中的谓词单调,所以二分查找有效。那么答案就是x 在任何s_i 中的最小继承者。

复杂性O(n log n) 预处理和O(log^3 n) 每个查询。

因此,对于 q 查询,我们总共获得了 O(n log n + q log^3 n) 的运行时间。我相信我们可以通过更聪明的选择算法将其归结为O(q log^2 n)

更新:如果我们正在寻找一种可以一次处理所有查询的离线算法,我们可以使用以下算法获得O((n + q) * log n * log (q + n))

  • 预处理所有查询,创建一组曾经出现在数组中的所有值。数量最多为q + n
  • 构建一个段树,但这次不是在数组上,而是在一组可能的值上。
  • 段树中的每个节点都代表一个值区间,并维护一组这些值出现的位置。
  • 要回答查询,请从段树的根开始。检查根的左孩子中有多少位置位于查询区间内(我们可以通过在位置的 BBST 中进行两次搜索来做到这一点)。让这个数字是m。如果k <= m,递归到左孩子。否则递归到右孩子,k 递减m
  • 对于更新,从覆盖旧值的O(log (q + n)) 节点中删除位置,并将其插入到覆盖新值的节点中。

这种方法的优点是我们不需要子树大小,因此我们可以使用平衡二叉搜索树的大多数标准库实现来实现这一点(例如 C++ 中的set<int>)。

我们可以通过将分段树更改为weight-balanced tree such as a BB[α] tree 来将其变成在线算法。它与其他平衡二叉搜索树一样具有对数运算,但允许我们在它变得不平衡时通过将重建成本计入必然导致不平衡的操作来从头开始重建整个子树。

【讨论】:

  • 请注意,为了有效地提供,.rank() 需要扩充 BBST,例如,每个节点将子节点的数量存储在其左子节点中。
  • @David Eisenstat:是的,BBST 必须有O(log n) rank/insert/delete 并且必须支持多个相等的节点。它还应该支持线性时间合并,以便我们获得O(n log n) 时间来构建初始段树(但这不是太重要)
  • 可以通过与数组索引配对来解决相等的值,但仅靠排名就失去了大多数标准库实现的资格。
  • @David:果然。再见multiset<int>TreeSet,你好自己的BBST 实现。好东西可以在
  • 我的观点是,它是一种非常有用的装饰,应该让更多的图书馆设计师看到它的价值。
【解决方案2】:

如果这是一个编程竞赛问题,那么您也许可以使用以下 O(n log(n) + q n^0.5 log(n)^1.5) 时间算法。它被设置为很好地使用 C++ STL,并且由于使用更少的空间和间接性,它比 Niklas 的(以前的?)答案具有更好的 big-O 常量。

将数组分成 k 个长度为 n/k 的块。将每个块复制到第二个数组的相应位置并对其进行排序。更新:将更改的块复制到第二个数组中并再次排序(时间 O((n/k)log(n/k))。查询:复制到临时数组中最多 2 个(n/k - 1) 属于与查询间隔部分重叠的块的元素。对它们进行排序。使用this question 的答案之一从排序的临时数组和完全重叠的块的并集中选择请求等级的元素,及时O(k log(n/k)^2)。理论上k的最佳设置是(n/log(n))^0.5。使用Frederickson的复杂算法可以刮掉另一个log(n)^0.5和约翰逊。

【讨论】:

  • 在编程竞赛中,我们可以使用更简单的O((q + n) * log^2 (q + n)) 离线方法,我现在在更新我的答案中概述了这种方法。它也不需要增加子树大小:) 另外我可以问一下 Bentley 的算法是什么?它是“天真的”range query 实现吗?什么是更节俭的选择?在权重平衡的范围树中使用分数级联来支持更新?
  • @NiklasB。是的,这是天真的范围查询实现。 Chazelle 将空间需求降低为线性,此后几位作者略微改进了查询时间。
  • 我觉得这个方案比构建BST的segment tree更容易理解,也更容易实现..
  • @cegprakash 段树的工作量比听起来要少,但也许。
  • 我在 Segment Tree 上工作不多。但当然我已经尝试过 RMQ 问题。 Niklas 的解决方案对于像我这样的初学者来说太难理解了。他更关注空间复杂性和时间复杂性,而不是解释他做什么以及为什么做这些事情。
【解决方案3】:

对桶排序进行修改:创建一个包含所需范围内数字的桶,然后仅对该桶进行排序并找到第 k 个最小值。

【讨论】:

  • 不确定您要如何在此处实现桶排序 - 范围未知。无论如何,这将在线性时间性能和线性空间中得到最好的结果,其中选择算法在很少额外空间的情况下获得线性时间性能。我相信 OP 是在亚线性时间性能算法之后,以更新期间的亚线性操作为代价的。
【解决方案4】:

该死,这个解决方案无法更新元素,但至少找到了第 k 个元素,在这里你会得到一些想法,以便你可以想到一些提供更新的解决方案。尝试基于指针的 B 树。

这是 O(n log n) 空间和 O(q log^2 n) 时间复杂度。后来我用 O(log n) 对每个查询进行了解释。

因此,您需要执行以下操作:

1) 在给定数组上创建一个“段树”。

2) 对于每个节点,您将存储整个数组,而不是存储一个数字。该数组的大小必须等于它的孩子的数量。该数组(如您所料)必须包含底部节点(子节点或该段中的数字)的值,但已排序。

3) 要制作这样一个数组,您需要合并来自段树的两个子节点的两个数组。但不仅如此,对于刚刚创建的数组中的每个元素(通过合并),您需要记住数字在插入合并数组之前的位置(基本上,它来自的数组,以及其中的位置) .以及指向未从同一数组插入的第一个下一个元素的指针。

4) 使用这种结构,您可以检查在某些段 S 中有多少小于给定值 x 的数字。您可以找到(通过二分查找)根节点数组中的第一个数字 > = x。然后,使用您制作的指针,您可以在 O(1) 中找到两个子数组(前一个节点的子节点的数组)的相同问题的结果。您停止对代表给定段 S 内部或外部的完整段的每个节点进行此降序操作。时间复杂度为 O(log n): O(log n) 以找到 >=x 的第一个元素, 和 O(log n) 对于 S 的所有分解段。

5) 对解决方案进行二分搜索。

这是每个查询 O(log^2 n) 的解决方案。但是你可以减少到 O(log n)

1) 在执行我上面写的所有操作之前,您需要转换问题。您需要对所有数字进行排序并记住原始数组中每个数字的位置。现在这些位置代表您正在处理的数组。称该数组为 P。

如果查询段的边界是 a 和 b。您需要按值(而不是按索引)找到 P 中位于 a 和 b 之间的第 k 个元素。该元素代表您的结果在原始数组中的索引。

2) 要找到第 k 个元素,您需要进行某种类型的回溯,其复杂度为 O(log n)。您将按值询问索引 0 和(某些其他索引)之间的元素数量,这些元素介于 a 和 b 之间。

3) 假设您知道某个段 (0,h) 的此类问题的答案。为树中从 h 开始的所有部分获取相同类型问题的答案,从最大的部分开始。只要当前答案(来自段 (0,h))加上您最后得到的答案大于 k,就继续获得这些答案。然后更新 h。不断更新 h,直到树中只有一个以 h 开头的段。那 h 是你在你所说的问题中寻找的数字的索引。

要从树中获得某个片段的此类问题的答案,您将花费 O(1) 的时间。因为你已经知道它的父段的答案,并且使用我在第一个算法中解释的指针,你可以在 O(1) 中得到当前段的答案。

【讨论】:

    猜你喜欢
    • 2022-10-15
    • 2013-11-14
    • 2021-03-04
    • 1970-01-01
    • 1970-01-01
    • 2016-09-25
    • 2019-08-13
    • 2013-03-26
    • 2016-03-06
    相关资源
    最近更新 更多