大范围连续整数的数据结构?

时间:2011-10-04 07:52:07

标签: python algorithm tree computer-science

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

我很确定第二次操作是在第一次操作的正确实现的情况下解决的。

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

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

我现在正在研究AVL,红黑和间隔树,二进制堆和treaps。比较他们的属性是压倒性的。我应该使用哪种结构?

编辑(问题澄清):我对如何存储这些值以及创建数据结构非常灵活。关于我如何接收来自其他应用程序的输入,我不灵活,如下所示:CATEGORY(cat3, I, J)。我当前的解决方案创建了一个树,其中包含该范围中每个整数的节点。这对于我的数据集的大小来说太慢了,所以如果给出更好的方法,我很乐意重新设计。

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

5 个答案:

答案 0 :(得分:2)

据我所知,你有一个范围[A,B]和表格的查询 -

  1. 将特定范围指定给类别
  2. E.g.
    R1 R2 C1
    R3 R4 C2
    
    1. 查询特定类别中项目总数的范围。 例如。查找R1 R4中的类别数
    2. 使用上面给出的字典的简单实现不会像我在本例中所描述的那样起作用 -

      假设我们有一个范围[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)

      在这种情况下,最后一次分配(1 5000 C5555)将需要删除类别C1,C2 ... C4999中相应集合的先前范围

      OR

        

      1:{“stop”:5,“category”:“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,...)

      您可以为每个节点分配一个值“category”,并给定一个间隔遍历适当划分间隔的树(例如,2500:3000和3500:4500之间的2500到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。接下来,它将设置3:4所需的children_update为true,以便将来更新其子项(在这种情况下已经设置好了......不会造成伤害)。

答案 1 :(得分:0)

您可以使用以下格式的普通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)时间内“放置”范围的开始,然后一切都可以在恒定时间内完成,除了将新密钥插入到新的密钥所需的O(n)时间之外。 list with bisect.insort_left。

答案 2 :(得分:0)

您可以非常轻松地在连续整数的数组上构建树结构,这应该有助于您的常数因子。首先将序列重新编号为从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时间内有效。

答案 3 :(得分:0)

假设:

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

(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)

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

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

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

这些词典应该可以很容易地获得O(日志)时间的计数。我们只需要在相关序列的开始和停止的路径上添加正确的数字。

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

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