选择点数最多的点的最有效方法

时间:2017-06-04 02:12:21

标签: algorithm geometry grouping linear-algebra range-tree

N.B:问题的底部有一个重要的编辑 - 检查出来

问题

说我有一套要点:

Points

我希望找到围绕它的点数最多的点,在半径R(即圆圈)内或在+-R(即正方形)的2维点内。我将它称为最密集的函数。

对于此问题中的图表,我将周围区域表示为圆圈。在上图中,中间点的周围区域以绿色显示。此中间点具有半径R内所有点的最大周围点,并且将由最密集的点函数返回。

我尝试了什么

解决此问题的可行方法是使用范围搜索解决方案; this answer进一步解释,并且“ O(log(n) + k)最坏情况时间”。使用它,我可以获得每个点周围的点数,并选择具有最大周围点数的点。

然而,如果积分非常密集(大约一百万),那么:

enter image description here

然后,这些百万点(1e6)中的每一个都需要进行范围搜索。最差情况时间O(log(n) + k),其中 k 是范围内返回的点数,对于以下点树类型为真:

  • 二维的kd-trees(实际上稍差,O(sqrt(n)+k)),
  • 2d-range trees,
  • 四叉树,最坏情况下的时间为O(n)

因此,对于组内所有点的半径1e6内的一组R点,它为每个点提供了O(log(1e6) + 1e6)的复杂性。这产生了超过一万亿次的操作!

有关实现这一目标的更有效,更精确的方法的任何想法,以便我能够在合理的时间内(最好O(n log(n))或更少)找到一组点数最多的点数?

修改

原来上面的方法是正确的!我只需要帮助实现它。

(半)溶液

如果我使用二维范围树:

  • 范围报告查询费用O(log^d(n) + k),用于k返回的点
  • 对于具有分数级联的范围树(也称为分层范围树),复杂度为O(log^(d-1)(n) + k)
  • 对于2个维度,即O(log(n) + k)
  • 此外,如果我执行范围计数查询(即,我不报告每个点),则费用为O(log(n))

我会在每一点上执行此操作 - 产生我想要的O(n log(n))复杂度!

问题

但是,我无法弄清楚如何为2d分层范围树的计数查询编写代码。

我找到了一个关于范围树的great resource (from page 113 onwards),包括2d-range树的伪代码。但我无法弄清楚如何引入分数级联,也不知道如何正确实现计数查询,使其具有O(log n)复杂度。

我还在Java中找到了两个范围树实现herehere,在C ++ here中找到了一个,尽管我不确定它是否使用了分数级联,如上所述

countInRange方法
  

在最坏的情况下返回这些点的数量       * O(log(n)^ d)时间。在最坏的情况下,它也可以返回矩形中的点       * O(log(n)^ d + k)时间,其中k是矩形中的点数。

这对我来说意味着它不适用于分数级联。

精致问题

为了回答上面的问题,我需要知道的是,是否存在具有分数级联的2d范围树的库,其具有复杂度O(log(n))的范围计数查询,所以我不会重新发明任何轮子,或者你能帮助我编写/修改上面的资源来执行复杂性的查询吗?

如果你能提供任何其他方法来实现O(log(n))中任意其他方式的2d点数的范围计数查询,也不要抱怨!

3 个答案:

答案 0 :(得分:1)

我首先要创建像https://en.wikipedia.org/wiki/K-d_tree这样的东西,在树上有一个带叶点的树,以及每个节点有关其后代的信息。在每个节点,我会保留一个后代数的计数,以及一个包围这些后代的边界框。

现在,对于每一点,我都会递归搜索树。在我访问的每个节点上,要么所有的边界框都在当前点的R内,所有的边界框都比当前点的R大,或者其中一些在R内,一些在R外。在第一个case我可以使用当前节点的后代数量的计数来增加当前点的R内的点数,并返回递归的一个级别。在第二种情况下,我可以简单地返回一级递归而不增加任何东西。只有在中间情况下我需要继续递归树。

因此,我可以为每个点计算R中邻居的数量,而不检查其他每个点,并选择具有最高计数的点。

如果点均匀分布,那么我认为你最终将构建一个kd树,其中较低的水平接近常规网格,我认为如果网格的大小为A x A,那么在最坏的情况下R足够大,以便它的边界是一个与O(A)低级单元格相交的圆,所以我认为如果你有O(n)个点你可以预期这会花费大约O(n * sqrt(n))。 / p>

答案 1 :(得分:1)

我建议使用plane sweep algorithm。这允许一维范围查询而不是二维查询。 (哪个更有效,更简单,并且在方形邻域的情况下不需要分数级联):

  1. 按Y坐标对数组S进行排序。
  2. 向阵列S前进3个指针:当前检查(中心)点的一个(C);另一个,距离最近点的A(略微前进)> R低于C;最后一个,B(略微落后)距离最远点< R高于它。
  3. 将A指向的点插入Order statistic tree(按坐标X排序),并从此树中删除B指向的点。使用此树从C到左/右距离R处找到点,并使用这些点的差异'树中的位置以获得C周围方形区域的点数。
  4. 使用上一步的结果选择"大多数包围"点。
  5. 如果您旋转点(或只是交换X-Y坐标)以使占用区域的宽度不大于其高度,则可以优化此算法。您还可以将点切割成垂直切片(具有R大小的重叠)并分别处理切片 - 如果树中有太多元素,使其不适合CPU缓存(这不太可能只有100万个点)。该算法(优化与否)具有时间复杂度O(n log n)。

    对于圆形邻域(如果R不太大且点均匀分布),您可以用几个矩形近似圆:

    approximated circle

    在这种情况下,算法的第2步应使用更多指针,以允许插入/移出多个树。在第3步,您应该在适当距离(< = R)附近的点附近进行线性搜索,以区分圆内的点与其外的点。

    处理圆形邻域的其他方法是近似具有相同高度的矩形的圆(但是这里的圆应该被分成更多的部分)。这导致了更简单的算法(使用排序数组而不是订单统计树):

    1. 将点占据的区域切割为水平切片,按Y排序切片,然后按切片对切片内的点进行排序。
    2. 对于每个切片中的每个点,假设它是一个"中心"指出并做第3步。
    3. 对于每个附近的切片使用二进制搜索来找到欧几里德距离接近R的点,然后使用线性搜索来告诉"内部"来自"外部"那些。停止线性搜索,其中切片完全在圆内,并通过阵列中位置的差异计算剩余点。
    4. 使用上一步的结果选择"大多数包围"点。
    5. 该算法允许前面提到的优化以及分数级联。

答案 2 :(得分:0)

您可以通过在O(n)时间预处理数据来加速您使用的任何算法,以估算相邻点的数量。

对于半径 R 的圆,创建一个网格,其单元格在x方向和y方向都具有 R 维度。对于每个点,确定它属于哪个单元格。对于给定的单元格 c ,此测试很简单:

c.x<=p.x && p.x<=c.x+R && c.y<=p.y && p.y<=c.y+R

(您可能想深入思考封闭或半开放区间是否正确。)

如果您具有相对密集/同质的覆盖范围,则可以使用数组来存储值。如果coverage是稀疏/异构的,您可能希望使用散列映射。

现在,考虑网格上的一个点。单元内一个点的极值位置如下所示:

Grid of radius R

细胞角落的点只能是四个细胞中有点的邻居。沿着边缘的点可以是具有六个单元中的点的邻居。不在边缘上的点是具有7-9个单元格中的点的邻居。由于一个点很少精确地落在角落或边缘上,我们假设焦点单元格中的任何点都是与所有8个周围单元格中的点相邻的。

因此,如果 p 点位于单元格(x,y)中,N[p]会识别p内的Np[y][x]个邻居数radius R N[p]表示单元格(x,y)中的点数,然后N[p] = Np[y][x]+ Np[y][x-1]+ Np[y-1][x-1]+ Np[y-1][x]+ Np[y-1][x+1]+ Np[y][x+1]+ Np[y+1][x+1]+ Np[y+1][x]+ Np[y+1][x-1] 由下式给出:

O(n)

一旦我们估算了每个点的邻居数量,我们就可以将该数据结构堆积到best_found时间的最大值(例如make_heap)。该结构现在是一个优先级队列,我们​​可以按照估计的邻居数排序每个查询的 O(log n)时间点。

对第一点执行此操作并使用 O(log n + k)圈搜索(或更聪明的算法)来确定该点的实际邻居数。在变量N[p]中记下此点并更新其N[best_found]值。

窥视堆顶部。如果估计的邻居数量小于parameters = (testid, username, fullname, firstnumber, secondnumber, floatnumber, posts, country, bio, link) query = """ INSERT INTO info VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)""" cur.execute(query, parameters) ,那么我们就完成了。否则,请重复上述操作。

要改进估算,您可以使用更精细的网格,如下所示:

Grid of radius R/2

以及一些巧妙的滑动窗口技术,以减少所需的处理量(例如,对于矩形情况,请参阅this answer - 对于圆形窗口,您应该使用一组FIFO队列)。为了提高安全性,您可以随机化网格的原点。

再次考虑你提出的例子:

Tricky neighbour-counting example with grid overlaid

很明显,这种启发式方法有可能节省大量时间:使用上述网格,只需要执行一次昂贵的检查,以证明中间点具有最多的邻居。同样,更高分辨率的网格将改善估算并减少需要进行的昂贵支票的数量。

你可以而且应该使用类似的边界技术与mcdowella's answers一起使用;然而,他的答案并没有提供一个开始寻找的好地方,所以可以花很多时间去探索低价值点。