【问题标题】:What is the most efficient algorithm/data structure for finding the smallest range containing a point?找到包含点的最小范围的最有效算法/数据结构是什么?
【发布时间】:2019-01-19 21:34:33
【问题描述】:

给定一个包含数百万个价格范围的数据集,我们需要找到包含给定价格的最小范围。
以下规则适用:

  • 范围可以完全嵌套(即 1-10 和 5-10 有效)
  • 范围不能部分嵌套(即 1-10 和 5-15 无效)

示例:
鉴于以下价格范围:

  • 1-100
  • 50-100
  • 100-120
  • 5-10
  • 5-20

搜索价格7的结果应该是5-10
搜索价格100的结果应该是100-120(包含100的最小范围)。

实现这一点最有效的算法/数据结构是什么?
在网上搜索,我只找到了在范围内搜索范围的解决方案。
我一直在研究莫顿计数和希尔伯特曲线,但不知道如何在这种情况下使用它们。
谢谢。

【问题讨论】:

  • 有一个明显的 O(n) 算法,是你要找的吗?
  • 如果您的问题是索引数据以进行高效查询,那么您应该查看en.wikipedia.org/wiki/Interval_treeen.wikipedia.org/wiki/Segment_tree
  • 您是否要搜索多个价格点,是否已经订购了细分市场?每个查询的价格点也会有不同的细分输入吗?
  • @pLOPeGG 当然,O(n) 是显而易见的答案,但我正在寻找一个更有效的答案(也许 O(logn)?)
  • @SaiBot 区间树是一个有趣的解决方案,但是当嵌套范围很多时它开始失去效率

标签: algorithm hilbert-curve


【解决方案1】:

因为你没有提到这个 ad hoc 算法,所以我将这个作为你问题的简单答案:

这是一个 python 函数,但它很容易理解并将其转换为另一种语言。

def min_range(ranges, value):
    # ranges = [(1, 100), (50, 100), (100, 120), (5, 10), (5, 20)]
    # value = 100

    # INIT
    import math
    best_range = None
    best_range_len = math.inf

    # LOOP THROUGH ALL RANGES
    for b, e in ranges:

        # PICK THE SMALLEST
        if b <= value <= e and e - b < best_range_len:
            best_range = (b, e)
            best_range_len = e - b

    print(f'Minimal range containing {value} = {best_range}')

我相信有更有效和更复杂的解决方案(例如,如果您可以进行一些预计算),但这是您必须采取的第一步。

编辑:这是一个更好的解决方案,可能在 O(log(n)) 中,但它不是微不足道的。它是一棵树,其中每个节点都是一个区间,并且有一个包含在他内部的所有严格非重叠区间的子列表。 预处理在 O(n log(n)) 时间内完成,在最坏的情况下查询是 O(n)(当你找不到 2 个不重叠的范围时),平均可能是 O(log(n))。

2 类:保存树并且可以查询的树:

class tree:
    def __init__(self, ranges):
        # sort the ranges by lowest starting and then greatest ending
        ranges = sorted(ranges, key=lambda i: (i[0], -i[1]))
        # recursive building -> might want to optimize that in python
        self.node = node( (-float('inf'), float('inf')) , ranges)

    def __str__(self):
        return str(self.node)

    def query(self, value):
        # bisect is for binary search
        import bisect
        curr_sol = self.node.inter
        node_list = self.node.child_list

        while True:
            # which of the child ranges can include our value ?
            i = bisect.bisect_left(node_list, (value, float('inf'))) - 1
            # does it includes it ?
            if i < 0 or i == len(node_list):
                return curr_sol
            if value > node_list[i].inter[1]:
                return curr_sol
            else:
                # if it does then go deeper
                curr_sol = node_list[i].inter
                node_list = node_list[i].child_list

保存结构和信息的节点:

class node:
    def __init__(self, inter, ranges):
        # all elements in ranges will be descendant of this node !
        import bisect

        self.inter = inter
        self.child_list = []

        for i, r in enumerate(ranges):
            if len(self.child_list) == 0:
                # append a new child when list is empty
                self.child_list.append(node(r, ranges[i + 1:bisect.bisect_left(ranges, (r[1], r[1] - 1))]))

            else:
                # the current range r is included in a previous range 
                # r is not a child of self but a descendant !
                if r[0] < self.child_list[-1].inter[1]:
                    continue
                # else -> this is a new child
                self.child_list.append(node(r, ranges[i + 1:bisect.bisect_left(ranges, (r[1], r[1] - 1))]))

    def __str__(self):
        # fancy
        return f'{self.inter} : [{", ".join([str(n) for n in self.child_list])}]'

    def __lt__(self, other):
        # this is '<' operator -> for bisect to compare our items
        return self.inter < other

并进行测试:

ranges = [(1, 100), (50, 100), (100, 120), (5, 10), (5, 20), (50, 51)]
t = tree(ranges)
print(t)
print(t.query(10))
print(t.query(5))
print(t.query(40))
print(t.query(50))

【讨论】:

  • 这是一个显而易见的算法,但在最坏的情况下它是 O(n)。我们需要一个有效的方法
  • 哦,好吧,那就考虑SaiBot方法。另一个解决方案是因为没有重叠,您可以构建一棵树,其中每个节点都是一个范围,并且有一个范围列表作为子节点。在此列表中仅放置直接包含在父范围等中的范围。要查询这棵树,从根开始,在其子列表中的右子范围内进行二分查找。如果没有人 -> 当前范围是您的答案,否则更深入并重复。根据您的数据,它可能会更快(或更慢)。
【解决方案2】:

产生不连贯区间的预处理
(我将源段称为范围,将结果段称为区间)

永远范围边界(开始和结束)制作元组:(值,开始/结束字段,范围长度,id),将它们放入数组/列表中

按第一个字段对这些元组进行排序。在平局的情况下,左为开始,右为结束。

Make a stack
Make StartValue variable.
Walk through the list:
     if current tuple contains start:
          if interval is opened:   //we close it
             if  current value > StartValue:   //interval is not empty
                  make interval with   //note id remains in stack
                      (start=StartValue, end = current value, id = stack.peek)       
                  add interval to result list
          StartValue = current value //we open new interval
          push id from current tuple onto stack
     else:   //end of range
             if  current value > StartValue:   //interval is not empty
                 make interval with    //note id is removed from stack
                      (start=StartValue, end = current value, id = stack.pop)
                 add interval to result list
         if stack is not empty:
              StartValue = current value //we open new interval

之后,我们对包含源范围的开始/结束值和 id 的不相交区间列表进行了排序(请注意,许多区间可能对应于相同的源范围),因此我们可以轻松地使用二分搜索。

如果我们以嵌套顺序(嵌套在它的父级之后)一个接一个地添加源范围,我们可以看到每个新范围最多可能生成两个新区间,因此区间的总数M &lt;= 2*N 和总体复杂度为O(Nlog N + Q * logN) 其中 Q 是查询数

编辑: 添加if stack is not empty部分

您的示例 1-100、50-100、100-120、5-10、5-20 的结果是

1-5(0), 5-10(3), 10-20(4), 20-50(0), 50-100(1), 100-120(2) 

【讨论】:

  • 这是 log(m) 最坏的情况,m 是使它们不相交后剩下的范围数量。如果你能证明m和n在同一个阶,我认为这是最好的解决方案
  • @juvian 是的,新的间隔可能只出现在源范围的起点和终点,所以m~n。添加到答案。
【解决方案3】:

由于pLOPeGG已经涵盖了ad hoc案例,所​​以我将在执行预处理以有效支持多个查询的前提下回答这个问题。

有效查询区间的通用数据结构是Interval TreeSegment Tree

【讨论】:

  • 我认为这可能行不通,或者可能比您想象的更难实施。问题是,一旦您找到小于查询的最大起始值,最终结果可能不会以这个最大值开始。这是一个例子:ranges = [(1, 100), (5, 20), (5, 10), (50, 100)] 如果我们查询 30,那么最大的声明值为 5,但正确的答案是(1, 100) 这意味着一些困难可能会阻止我们获得这个 O(log(n)) 计算时间。
  • @pLOPeGG 你说得对,我必须回到白板;-) 我现在要删除答案的第二部分。
【解决方案4】:

这样的方法怎么样。因为我们只允许嵌套而不是部分嵌套。这看起来是一种可行的方法。

  • 将段拆分为 (left,val)(right,val) 对。
  • 根据vals 和左/右关系对它们进行排序。
  • 使用二分搜索搜索列表。我们得到两个未找到和未找到的结果。
  • 如果找到,请检查它是左还是右。如果它是左向右走,直到找到右而不找到左。如果是右向左走,直到找到左而不找到右。选择最小的。
  • 如果high-low为1或0时未找到则停止。然后将查询到的值与您所在节点的值进行比较,然后像以前一样左右搜索。

举个例子;

我们会在搜索时使用(l,10) (l,20) (l,30) (r,45) (r,60) (r,100) 说,65 你落在(r,100) 所以你向左走,找不到带有(l,x) 的位置,这样x&gt;=65 所以你向左走直到你平衡左和右,第一个右和最后一个左是你的间隔。再处理部分会很长,但因为你会保持这种状态。在最坏的情况下,它仍然是O(n)。但最坏的情况要求您将所有内容都嵌套在彼此内部,然后搜索最外层。

【讨论】:

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