【问题标题】:Data structure for large ranges of consecutive integers?大范围连续整数的数据结构?
【发布时间】:2011-11-30 11:29:27
【问题描述】:

假设您在内存中有大量连续的整数,每个整数都恰好属于一个类别。两个操作必须是 O(log n):将一个范围从一个类别移动到另一个类别,并找到给定范围的类别计数。

如果第一个操作的正确实现,我很确定第二个操作可以轻松解决。

每个整数都从一个类别开始,所以我从一组平衡的 BST 开始。将子树从一个 BST 移动到另一个(例如,将范围移动到不同的类别)的运行时间相当于合并两个 BST,即 O(n1 * n2)[1]。

这太慢了(在 python 中,C 不是一个选项),我想不出一种方法来利用我的数据的固有结构来创建高效的 BST 合并操作。

我现在正在研究 AVL、红黑和区间树、二叉堆和树堆。比较它们的属性是压倒性的。我应该使用哪种结构?

编辑(问题说明):我可以灵活地存储这些值并创建我的数据结构。对于如何接收来自另一个应用程序的输入,我很不灵活,如下所示:CATEGORY(cat3, I, J)。我当前的解决方案为范围内的每个整数创建一个带有节点的树。这对于我的数据集的大小来说太慢了,所以如果有更好的方法,我很乐意重新架构。

任何给定的请求都可以将任何可能的整数范围移动到任何类别中。换句话说,范围在CATEGORY(cat1, 1, 10) 后跟CATEGORY(cat3, 5, 15) 的意义上是重叠的,但在任何给定时间每个整数都将属于一个类别的意义上是非重叠的。

【问题讨论】:

  • 范围是保存为整数列表还是像(begin, end)这样的元组?
  • 在你开始实现一些树之前,你应该尝试使用内置的数据类型,比如字典和集合。这些都经过高度优化且非常高效。
  • 所以你有一个包含元组的列表 [ (number1, cat1), .... ] ???
  • 我不确定我是否完全理解这个问题,所以,我先问几个问题。你如何表示一个范围?作为某些数据结构中的所有整数还是只是第一个+最后一个(或第一个+计数)?在执行这些操作时,您是否曾经需要拆分和连接范围?什么是类别?可以用一些数字来描述吗?在类别之间移动是什么意思?
  • 通过将范围从一个类别移动到另一个类别,您的意思是对于范围 [x, y] 1) 所有整数 x 在其他类别中 会在新类别中?

标签: python algorithm tree computer-science


【解决方案1】:

据我了解,您有一个范围 [A, B] 和形式的查询 -

  1. 将特定范围分配给类别
例如。 R1 R2 C1 R3 R4 C2
  1. 查询特定类别中项目总数的范围。 例如。查找 R1 R4 中的类别计数

使用上面给出的字典的简单实现不会像我在这个例子中描述的那样工作 -

假设我们有一个范围 [1000, 5000]

我们进行如下分配-

1 2 C1 2 3 C2 3 4 C3 …… 4999 5000 C4999

现在我们进行以下分配

1 5000 C5555

这将对先前分配的范围 O(N) 的范围进行更新/更改/删除,其中 N 是范围的最大大小 (B - A)

D['category'] = set(of_all_the_ranges_you_have_in_category)

在这种情况下,上次分配需要从 C1、C2...C4999 类别中的相应集合中删除以前的范围 (1 5000 C5555)

1:{“停止”:5,“类别”:“C1”}, 6:{“停止”:19,“类别”:“C23”},

最后一次分配 (1 5000 C5555) 需要更新每个起始值 (1,2,3,4...,4999) 的类别

在 O(lg n) 中更新范围的更好选择是分段树 (http://en.wikipedia.org/wiki/Segment_tree)

对于上面的例子,段树看起来像这样

                   1000:5000
                      |
             ---------------------
             |                   |
           1000:3000         3001:5000
            |                    |
    ----------------      --------------------
   |               |      |                  |
 1000:2000     2001:3000   3001:4000       4001:5000

.................................................. ..................... ..................................................... ......等等

叶节点将具有范围 (1:2, 2:3,...)

您可以为每个节点分配一个值“类别”并给定一个区间遍历树以适当地划分区间(例如,对于 2500 到 4500 划分为 2500:3000 和 3001:4500 并在两个方向上继续,直到具有匹配范围的节点已找到)。

现在有趣的事情是在需要时更新节点的子节点。例如,在执行 1 5000 C5555 之类的任务时,不要立即更新子项。这个东西叫做惰性传播,你可以在这里(http://www.spoj.pl/forum/viewtopic.php?f=27&t=8296)了解更多。

现在是查询部分。如果类别的数量非常少,可以在每个节点维护一个频率表,并在需要时更新范围并在需要时惰性传播,否则,您将不得不从叶子到节点遍历整个树,计数和复杂度将变为 O (n)。

我认为可能存在更好的查询解决方案。我没有想到。

更新 举个小例子吧。

范围 [1,8]

允许的类别 {C1, C2}

        1:8
     1:4         5:8
     1:2  3:4      5:6    7:8
 1:1 2:2 3:3 4:4  5:5 6:6 7:7 8:8

每个节点会有 3 个字段 [category, category_counts[], children_update_required = false]

1 5 C1

查询将被划分,节点 1:4 的类别将设置为 C1,children_update_required 将设置为 true,其子节点现在不会更新(请记住仅在需要时更新或延迟传播)。节点 5:5 的类别也将设置为 C1

3 4 C2

查询将沿着树向 3:4 传播(在到达 3:4 的过程中,1:2 和 3:4 的类别将更新为 C1,1:4 的 children_update_required 将设置为 false,1: 2 和 3:4 的 children_update_required 将设置为 true),现在将根据当前要求将 3:4 的类别更新为 C2。接下来它将设置 children_update required of 3:4 为 true 以供将来更新其子项(在本例中已设置..无害)。

【讨论】:

  • 如果我收到不同范围的请求,这会起作用吗?例如,CATEGORY(cat3, 1, 10) 然后CATEGORY(cat1, 5, 7)?段树需要是静态范围。是的,与范围(数百万个数字和数十万个范围)相比,类别的数量非常少(
  • 是的,它会的。范围确实将保持不变。您只需修改与相应节点关联的类别数据。我在现有答案中添加了一个小例子来澄清。我不知道这是否会有所帮助,但我在 C++ 中使用段树解决了一个问题(尽管没有使用延迟传播)。您可以参考那里的更新和查询代码。它们将根据要求进行更改。问题 - link 代码 - link
【解决方案2】:

您可以很容易地在连续整数数组上构建树结构,这应该有助于您的常数因子。首先将序列重新编号,从 0 开始,计算出大于序列范围的 2 的最小幂。

现在考虑以下由整数 0-7 组成的树,我可以将其保存为四个数组,每个数组水平排列。

            (all)
     0-3      4-7
   0-1 2-3  4-5   6-7
 0 1  2  3  4  5  6   7

给定一个数字和一个级别,我可以在数组中找到该级别的偏移量,只需根据级别将数字右移一个量。

在每个元素中,我可以放置一个标记,标记为“混合”或为树的该节点处或之下的每个元素提供类别。我可以通过沿着从树根到叶子的路径来确定节点属于哪个类别,只要我看到一个没有说“混合”的节点就停止。我可以在时间 lg n 中更改数字间隔的类别,因为我在每个级别最多有两个节点来表示类别:如果我有三个节点,其中两个节点将具有相同的父节点,我可以合并它们。您可能需要稍微摆弄一下边缘才能使相邻范围正确,但我认为这在 lg n 时间内有效。

【讨论】:

  • 这是一个非常有趣的方法!它需要更多的空间,但这不应该是一个问题。我希望这两个操作(类别移动和类别计数)都是 O(log n),正如您所建议的那样。
  • 这里唯一的新想法是树形结构,正如您所说,它为了简单而交换了最佳情况空间。在类别为“偶数”和“奇数”的情况下,它可能在空间上具有竞争力。对于类别计数,您需要将摘要信息存储在树节点上,但如果您可以使用您提到的任何树结构解决此问题,您应该能够解决此问题。
【解决方案3】:

假设:

  • 任何整数都只能属于一个类别,因此范围不能相交。
  • 传入范围内的所有整数都属于一个类别。
  • 有时您需要拆分范围以将子范围移动到不同的类别。

(start, end, category) 元组表示范围。范围不相交,因此您可以构建它们的树。它比单个整数树更经济。要对范围(即节点)进行排序,您可以只使用起始值,因为它是唯一的,不能属于另一个范围。

如果您必须将范围 [a, b] 移动到另一个类别,您必须:

扫描您的树并更新完全包含在[a, b] 范围内的每个节点/范围。这是一个简单的深度优先遍历。遍历期间

  • 如果是current_node.start < a or current_node.start > b,则停止搜索。
  • 如果current_node.start >= a and current_node.end > b,则必须将current_node一分为二; [current_node.start, b] 将属于一个新类别,其余的将属于其原始类别。
  • 如果是current_node.start < a and current_node.end <= b,则相反。

树搜索是对数的,节点分裂是 O(1)。

如果您的树过于分散,您可以考虑合并具有相邻范围的节点。这将始终是插入或拆分产生的父母和孩子;检查和连接似乎总是 O(1)。

【讨论】:

    【解决方案4】:

    我们可以将当前状态表示为:

    0:cat1 200:cat2 500: cat0 700:cat6 800:cat1
    

    这意味着 0-200 是 cat1,200-500 是 cat2,等等。我们将值存储在以起始编号为键的二叉搜索树中。每个节点还将包含一个字典,将类别映射到该节点的所有子节点的计数。

    那些字典应该可以很容易地在 O(log) 时间内获得计数。我们只需在相关序列的开始和结束的路径上添加正确的数字。

    当我们需要将序列 X-Y 设置为类别 Z 时该怎么办?

    1. 确定Y O(logn )的当前类别
    2. 移除 X -Y O(k) 之间的所有值,但由于插入这些节点的成本更高,我们可以将其称为 O(1) 摊销。
    3. 在 X O(log n) 处插入新节点
    4. 更新类别计数字典。我们应该只需要更新受影响部分的 O(log n) 父级,所以这是 O(log n)

    总共是 O(log n) 时间。

    【讨论】:

    • 你能澄清一下#2吗?我同意这是 O(k),但我不清楚它如何可以 O(1) 摊销。 k 的平均值与 O(n) 的阶数相同,这将使这一步成为 O(n)。
    • @knite,在考虑插入时将其摊销。每次删除之前都必须插入。因此,花费额外的 O(1) 来删除它与花费额外的 O(1) 来插入它是相同的。插入已经是 O(log n) 所以我们可以忽略它。
    • @knite,诚实地实现像这样的复杂数据结构在 python 中往往效果不佳。
    【解决方案5】:

    你可以有一个如下形式的普通 python 字典

    1 : { "stop" : 5, "category" : "C1"},
    6 : { "stop" : 19, "category" : "C23"},
    etc
    

    这里的键是范围的开始,值包含范围的结束和该范围所属的类别。

    因为字典有固定的时间来访问项目,所以您可以编写一些代码来轻松有效地将一个范围移动到另一个类别:在最坏的情况下,如果您的范围拆分了以前的范围,您将不得不以某种方式重组您的字典在两个或更多。例如,如果您想将 (4, 8) 的范围分配到另一个类别,您最终会得到:

    1 : { "stop" : 3, "category" : "C1"},
    4 : { "stop" : 8, "category" : "new category"},
    9 : { "stop" : 19, "category" : "C23"},
    etc
    

    查找类别计数是微不足道的,只需在恒定时间内收集所有您想要的范围并计算类别..

    编辑:要成功找到最低(最高)键以开始执行计算/更改,您还需要一个简单的 python 列表,其中所有键都已排序,以及 bisect 模块。这将有助于在列表中定位索引以在 O(logn) 时间内“放置”范围的开始,然后一切都可以在恒定时间内完成,除了将新键插入到使用 bisect.insort_left 列出。

    【讨论】:

    • 如何存储从同一个整数开始的两个范围?我的意思是如果你有范围 (4, 8) 和范围 (4, 10) 会发生什么?
    • @AndreaAmbu 来自问题:“每个都属于一个类别”
    • @rplnt 我想我错过了那里的主题,我知道每个 range 都属于特定类别,因为 OP 根据范围定义操作,但在那句话中,each 代表 number。对我来说看起来模棱两可,谢谢。
    • @AndreaAmbu - 范围不重叠。
    猜你喜欢
    • 1970-01-01
    • 2013-09-27
    • 1970-01-01
    • 1970-01-01
    • 2013-05-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-03-21
    相关资源
    最近更新 更多