给出一行中的区域列表:
regions = [(10,25), (18, 30), (45, 60), ...] # so on so forth, regions can be overlapping, of variable size, etc.
我想知道X点所属的区域:
x = 23
find_regions(regions, x) # ==> [(10, 25), (18, 30)]
我天真地知道(和我目前的实现)我们可以在O(n)中搜索,但是一个更具戏剧性的用例包含数千个区域(和数千个查找点,真的,是激励因素)有理由研究比这更快的方法:
regions = [(start, end) for (start, end) in regions if start < x and x < end]
我会冒险猜测有人在此之前解决了这个问题......但我不确定如何最好地完成它。想法?
答案 0 :(得分:2)
这是interval trees旨在完成的确切工作。谷歌搜索Python interval tree
打开了一个名为Banyan的现有库来实现它们,虽然我不能说它的可靠性,但似乎没有积极开发。您还可以实现自己的间隔树。
从N个区间列表构造区间树的预处理时间是O(Nlog(N)),与其他一些答案不同,它只需要O(N)空间,无论区间多少交叠。找出与给定点重叠的区间数的时间是O(M + log(N)),其中M是包含该点的区间数。
Banyan区间树演示,取自PyPI page:
>>> t = SortedSet([(1, 3), (2, 4), (-2, 9)], updator = OverlappingIntervalsUpdator)
>>>
>>> print(t.overlap_point(-5))
[]
>>> print(t.overlap_point(5))
[(-2, 9)]
>>> print(t.overlap_point(3.5))
[(-2, 9), (2, 4)]
>>>
>>> print(t.overlap((-10, 10)))
[(-2, 9), (1, 3), (2, 4)]
答案 1 :(得分:0)
我对列表理解的唯一更改是将其设为generator
,将比较缩短为start < x < end
,如果只需要一个,则可选择调用next()
:
>>> regions = [(10,25), (18, 30), (45, 60)]
>>> x = 23
>>> next((start, end) for (start, end) in regions if start < x < end)
(18, 30)
另请注意,您的比较start > x and x < end
有向后>
。应该是start < x and x < end
。该修复程序包含在我的答案中
编辑:看到关于二元搜索的评论和答案让我意识到我对缺乏改进空间当然是错误的。也就是说,为了通过next()
稍微改善比较和短路,我仍然会保留这个答案。但与二元搜索相比,我的改进是微不足道的。
我的搜索线性速度更快了。二进制是对数的。
答案 2 :(得分:0)
如果区域重叠,只需对区域进行排序并进行二分查找。
如果区域重叠,则对于每个重叠区域计算重叠区域列表并将其存储为列表。然后进行二分搜索。
例如:(1,10),(5,15) 转换为
(1,4), (5,10), (11, 15)
| | |
(1,10) (1,10) (5,15)
|
(5,15)
即链接(5,10)到它所属的区域。
注意:这些只是线索,你需要做更多的工作。
答案 3 :(得分:0)
我建议你将所有内容分成不重叠的基本区间,这样每个基本区间要么完全被覆盖,要么完全超出任何给定的区间。然后,您可以创建从基本区间到给定区间的地图。由于基本区间不重叠,因此您可以使用二分查找轻松找到匹配的区间。从那里你可以查找映射到它的实际间隔。 初始排序为O(N log N),构建映射为O(N),最终查找为O(log N),因为二进制搜索。基本区间的数量小于2 * N.
这是一个粗略的实现。不确定搜索点是否完全达到结束间隔结束的情况。
class IntervalFinder():
elem_list = [] # the borders of the elementary interval
elem_sets = [] # the actual intervals mapped to each elementary
def __init__( self, intervals ):
# sort the left ends
a = sorted( intervals )
# sort the right ends
b = sorted( intervals, key=lambda x : x[1] )
ia = 0 # index into a
start = a[0][0] # the start of the elementary interval
# the set of actual intervals covering the
# current elementary
current = set()
for xb in b:
while ia < len(a) and a[ia][0] < xb[1]:
stop = a[ia][0]
# an elementary interval ends here
# because a new interval starts
if stop > start:
self.elem_sets.append( set( current ) )
self.elem_list.append(start)
start = stop
current.add( a[ia] )
ia += 1
if start < xb[1]:
self.elem_sets.append(set(current))
self.elem_list.append(start)
start = xb[1]
current.remove( xb )
self.elem_sets.append(set())
self.elem_list.append(start)
def find( self, a ):
k = bisect.bisect( self.elem_list, a ) - 1
if k<0:
return set()
# if its exactly on the border
# it belongs to both the right and the left
if a == self.elem_list[k]:
h = set(self.elem_sets[k])
return h.union( self.elem_sets[k-1] )
else:
return self.elem_sets[k]
intervals = [ ( 1, 10), (5, 15), (10, 20), (5, 30) ]
ifind = IntervalFinder(intervals)
for x in [0, 4,5,9,10,11, 20, 25, 30, 35]:
print( x, ifind.find(x) )