【问题标题】:finding a set of ranges that a number fall in找到一个数字所在的一组范围
【发布时间】:2013-08-12 04:47:05
【问题描述】:

我有一个 20 万行的数字范围列表,例如 start_position、stop 位置。 除了不重叠的重叠之外,该列表还包括所有类型的重叠。

列表如下所示

  • [3,5]
  • [10,30]
  • [15,25]
  • [5,15]
  • [25,35]
  • ...

我需要找到给定数字所在的范围。并将重复 100k 个数字。 例如,如果 18 是上面列表的给定数字,那么函数应该返回 [10,30] [15,25]

我正在使用 bisect 以一种过于复杂的方式进行操作,任何人都可以提供有关如何以更快的方式进行操作的线索。

谢谢

【问题讨论】:

  • 输入范围内数字的最大范围是多少?在要查找的数字列表中?它们都是整数吗?

标签: python algorithm


【解决方案1】:

解决此问题的最佳方法是构建一个区间树。 (sza 给出的范围树是静态的。这意味着在你构建它之后你不能添加/删除节点。如果你没问题,那么他的回答就足够了。)

你可以在这里了解区间树:

Youtube:https://www.youtube.com/watch?v=q0QOYtSsTg4

维基:https://en.wikipedia.org/wiki/Interval_tree

区间树基本上是基于范围元组的左值的二叉树。 ([left, right]) 它的特别之处在于树中的每个节点都有一个名为 max 的值。最大值保存该节点下方的节点的最大右值,包括节点本身。换句话说,当前节点的最大值将设置为当前节点为根的子树的最大右值。

为了解决您的问题,我们也可以添加最小值。其中 min 值将保存该节点为根的所有子树的最小左值。

从视频中编辑的屏幕截图:(抱歉质量问题)

这意味着当我们查询您的号码时:

1) add current node to set of answers if number is inside range    
2) go left if number is less than max value of left child.    
3) go right if number is greater than min value of right child.

【讨论】:

    【解决方案2】:

    好的,这是一个树实现:

    import itertools
    
    class treeNode:
        def __init__(self, segments):
            self.value = None
            self.overlapping = None
            self.below = None
            self.above = None
            if len(segments) > 0:
                self.value = sum(itertools.chain.from_iterable(segments))/(2*len(segments))
                self.overlapping = set()
                belowSegs = set()
                aboveSegs = set()
                for segment in segments:
                    if segment[0] <= self.value:
                        if segment[1] < self.value:
                            belowSegs.add(segment)
                        else:
                            self.overlapping.add(segment)
                    else:
                        aboveSegs.add(segment)
                self.below = treeNode(belowSegs)
                self.above = treeNode(aboveSegs)
            else:
                self.overlapping = None
    
        def getOverlaps(self, item):
            if self.overlapping is None:
                return set()
            if item < self.value:
                return {x for x in self.overlapping if x[0]<=item and item<=x[1]} | self.below.getOverlaps(item)
            elif item > self.value:
                return {x for x in self.overlapping if x[0]<=item and item<=x[1]} | self.above.getOverlaps(item)
            else:
                return self.overlapping
    

    演示

    import random as r
    
    maxInt = 100
    numSegments = 200000
    rng = range(maxInt-1)
    lefts = [r.choice(rng) for x in range(0, numSegments)]
    segments = [(x, r.choice(range(x+1, maxInt))) for x in lefts]  # Construct a list of 200,000 random segments from integers between 0 and 100
    
    tree = treeNode(segments)  # Builds the tree structure based on a list of segments/ranges
    def testReturnSegments():
        for item in range(0,100000):
            item = item % maxInt
            overlaps = tree.getOverlaps(item)
    
    import cProfile
    cProfile.run('testReturnSegments()')
    

    这使用 200k 段,并找到 100k 数字的重叠段,并在我的 2.3 GHz Intel i5 上运行 94 秒。这是 cProfile 输出:

    输出

             1060004 function calls (580004 primitive calls) in 95.700 seconds
    
       Ordered by: standard name
    
       ncalls  tottime  percall  cumtime  percall filename:lineno(function)
            1    0.000    0.000   95.700   95.700 <string>:1(<module>)
    580000/100000   11.716    0.000   93.908    0.001 stackoverflow.py:27(getOverlaps)
       239000   43.461    0.000   43.461    0.000 stackoverflow.py:31(<setcomp>)
       241000   38.731    0.000   38.731    0.000 stackoverflow.py:33(<setcomp>)
            1    1.788    1.788   95.700   95.700 stackoverflow.py:46(testReturnSegments)
            1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
            1    0.004    0.004    0.004    0.004 {range}
    

    【讨论】:

    • 感谢您的努力,但我有点困惑,您根本没有使用第一个定义的 getOverlaps 函数,对吧?当我针对不同的段列表对其进行测试时,它似乎不起作用,您可以尝试使用 73 的 segments=[(72, 214), (2619, 2781)]。它为我返回空集
    • @svural:你对这两件事都是正确的。第一个 getOverlaps 是我忘记从以前的实现中取出的东西,您注意到了一个错误,现在已修复 - 请尝试新版本。
    【解决方案3】:

    这似乎是一个范围覆盖问题。由于您要处理大量查询,因此您需要尽快给出结果。有一个算法涉及到这个问题,你可以看看Segment Tree

    这个想法是首先根据给定的区间构建一个 Segment Tree,然后对于每个查询,您可以像 log(N) 一样快,其中 N 是区间数。

    要得到所有可能的k区间,首先找到覆盖了log(n)所有子区间的父节点,然后遍历其子节点获取所有k个区间。因此,检索给定数字的所有区间的时间复杂度以log(N) + O(k) 为界,其中k &lt;&lt; n

    当所有区间都包含给定数字时,此算法可能会变慢到O(N)。在这种情况下,由于输出的大小为N,因此不存在更好的解决方案。因为算法的复杂度必须至少与输出大小相同或更高。

    希望对你有帮助。

    【讨论】:

    • 搜索算法将是 O(log(N)),但是,您需要返回包含给定数字的所有范围,可能是 O(N)。此算法无法保证 O(lnN)。
    • @notbad 当我们谈论算法的复杂性时,我们通常谈论平均情况而不是最坏情况。是的,在这种情况下,算法会变差。
    • 通常会声明一个输出敏感的运行时间,例如,对于 k 个结果,O(log n + k)。
    • @DavidEisenstat 谢谢。已更新。
    【解决方案4】:
    import random as r
    rng = range(99)
    lefts = [r.choice(rng) for x in range(0, 200000)]
    segments = [(x, r.choice(range(x+1, 100))) for x in lefts]
    
    leftList = sorted(segments, key=lambda x:x[0])
    rightList = sorted(segments, key=lambda x:x[1])
    
    def returnSegments(item, segments, leftList, rightList):
        for leftN in range(len(leftList)):
            if leftList[leftN][0] > item:
                break
        for rightN in range(len(rightList)):
            if rightList[rightN][1] > item:
                break
        return list(set(leftList[:leftN]) & set(rightList[rightN:]))
    
    def testReturnSegments():
        for item in range(0,100):
            item = item % 100
            returnSegments(item, segments, leftList, rightList)
    

    此代码使用 200k 段和 100 个数字。它在我的 2.3 GHz i5 macbook 上运行只需 9.3 秒,这意味着完整的 200k x 100k 运行需要 2.5 小时。可能不够快,但可能有办法加快这种方法 - 我会考虑的。

    【讨论】:

      【解决方案5】:
      def get_containing_intervals(x):
          for start, end in intervals:
              if start <= x <= end:
                  yield x
      

      在此处使用 &lt;= 是基于区间包含(封闭式)的假设。

      如果您打算多次调用此函数,您可能需要进行某种预处理,例如@sza 的建议。

      【讨论】:

      • for 循环将永远持续下去,具体来说是 200k*100k 循环
      【解决方案6】:

      怎么样,

      1. 按第一列排序 O(n log n)

      2. 二分法查找超出 O(log n) 范围的索引

      3. 抛出超出范围的值

      4. 按第二列排序 O(n log n)

      5. 二分法查找超出 O(log n) 范围的索引

      6. 抛出超出范围的值

      7. 您只剩下范围内的值

      这应该是 O(n log n)

      您可以使用 np.sort 对行和列进行排序,二进制搜索应该只需要几行代码。

      如果您有很多查询,您可以保存第一个排序副本以供后续调用,但不能保存第二个。根据查询的数量,执行线性搜索可能比先排序再搜索更好。

      【讨论】:

        猜你喜欢
        • 2010-11-14
        • 2016-06-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-09-29
        相关资源
        最近更新 更多