【问题标题】:Count number of points inside a circle fast快速计算圆内的点数
【发布时间】:2010-12-23 06:28:09
【问题描述】:

给定平面上的一组 n 个点,我想以某种方式比 O(n^2) 更快地预处理这些点(最好是 O(nlog(n))),然后能够回答以下类型的查询“有多少 n 个点位于具有给定中心和半径的圆内?”比 O(n) 快(最好是 O(log(n))。

你能推荐一些我可以用来解决这个问题的数据结构或算法吗?

我知道这类问题通常可以使用 Voronoi 图来解决,但我不知道如何在这里应用它。

【问题讨论】:

  • 在预期的情况下你可能会做得很好,但如果你在 O(n) 下得到最坏情况下的查询性能,我会感到惊讶。我想到的最坏情况是很多点非常接近给定的圆。
  • @Jason,在这种情况下,所需的复杂度为 O(logn + k),其中 k 是圆内的点数。这实际上是计算几何课程中常见的家庭作业:)
  • 沿着 Voronoi 图的线,如果你画出所有点之间的垂直平分线,即 O(n^2),并确定你所做的所有区域(也是 O(n^2 ),根据research.att.com/~njas/sequences/A000124),那么每个区域中的所有点都按照距离的顺序共享相同的点序列,对吧?因此,对于每个区域,制作一个数据结构来解决该区域内的问题。这将使阶段 2 O(log n)。但在第 1 阶段,这是一个荒谬的工作量,我猜是 O(n^3 log n)。
  • @Anna,呵呵,这大大改变了问题。
  • @Anna 你能说一下你提到的常见家庭作业的常见解决方案是什么。虽然我需要比 O(k) 更快的算法,但它仍然很有用。

标签: algorithm math search geometry range


【解决方案1】:

构建空间细分结构,例如点的quadtreeKD-tree。在每个节点上存储该节点覆盖的点数。然后,当您需要计算查找圈所覆盖的点时,遍历树并为节点中的每个细分检查它是否完全在圈外,然后忽略它,如果它完全在圈内,则将其计数添加到如果它与圆相交,则总计,递归,当你到达叶子时,检查叶子内的点是否包含。

这仍然是 O(n) 最坏的情况(例如,如果所有点都位于圆周上),但平均情况是 O(log(n))。

【讨论】:

  • 好吧,在最坏的情况下,似乎没有办法比 O(n) 更快地做到这一点。然后平均 O(log(n)) 对我来说可能就足够了。谢谢!
  • 我在这里添加了使用 K-D 树的即用型实现 - leetcode.com/problems/…
【解决方案2】:

构建一个KD-tree 的点,这应该会给你比 O(n) 更好的复杂性,我认为平均 O(log(n))。

您可以使用二维树,因为这些点被限制在一个平面上。

假设我们已将问题转化为二维,我们将得到类似这样的点数:

 struct Node {
     Pos2 point;
     enum {
        X,
        Y
     } splitaxis;
     Node* greater;
     Node* less;
 };

greaterless 分别包含沿分割轴坐标较大和较小的点。

 void
 findPoints(Node* node, std::vector<Pos2>& result, const Pos2& origin, float radius) {
     if (squareDist(origin - node->point) < radius * radius) {
         result.push_back(node->point);
     }
     if (!node->greater) { //No children
          return;
     }
     if (node->splitaxis == X) {
         if (node->point.x - origin.x > radius) {
             findPoints(node->greater, result, origin radius);
             return;
         }
         if (node->point.x - origin.x < -radius) {
             findPoints(node->less, result, origin radius);
             return;
         }
         findPoints(node->greater, result, origin radius);
         findPoints(node->less, result, origin radius);
     } else {
         //Same for Y
     }
 }

然后你用KD树的根调用这个函数

【讨论】:

  • KD-trees 通常很有用,但我看不出它们在这种情况下有什么帮助。你能解释一下吗?
  • 感谢您的回答安德烈亚斯。你的代码找到了圆圈内的所有点,所以我认为它不能比 O(log(n)+k) 快,其中 k 是答案。但我只需要数字 k,而不需要点数。实际上,我知道如何修改它。我们需要在每个节点中存储子树的大小,如果子树内的所有可能点都位于圆圈内,我们只需将子树的大小添加到答案中,而不需要遍历子树。但我不确定在最坏的情况下这是否会比 O(N) 更好。
【解决方案3】:

如果我的目标是速度,并且点数不是很大(数百万),我会尽可能关注内存占用和算法复杂性。

不平衡的 k-d 树在纸面上是最好的,但它需要指针,这可以将内存占用扩大 3 倍以上,所以它被淘汰了。

平衡的 k-d 树不需要存储,除了每个点都有一个标量的数组。但它也有一个缺陷:标量不能量化——它们必须是与原始点相同的 32 位浮点数。如果它们被量化,则不再可能保证数组中较早出现的点在分裂平面上或在其左侧,并且在数组中较晚出现的点在分裂平面上或在它的右边。

我开发了一个数据结构来解决这个问题。 Synergetics 的人告诉我们,音量在体验上是四向的。假设飞机在体验上也是三向的。

我们习惯用四个方向-x、+x、-y和+y来穿越一个平面,但是使用三个方向a、b和c更简单,它们指向一个顶点等边三角形。

在构建平衡 k-d 树时,将每个点投影到 a、b 和 c 轴上。通过增加 a 对点进行排序。对于中点,向下取整,量化并存储a。然后,对于中位数左右的子数组,按b递增排序,对于中位数点,向下取整,量化,存储b。递归并重复,直到每个点都存储了一个值。

然后,当针对结构测试圆(或其他)时,首先计算圆的 最大值 a、b 和 c 坐标。这描述了一个三角形。在我们在上一段中制作的数据结构中,将中点的 a 坐标与圆的最大值 a 坐标进行比较。如果点的a大于圆的a,我们可以取消中位数之后的所有点。然后,对于中位数左右(如果不失格)的子数组,将圆的b与中点的b坐标进行比较。递归并重复,直到没有更多的点可以访问。

这在主题上与BIH data structure 相似,但不需要 -x 和 +x 以及 -y 和 +y 的间隔,因为 a、b 和 c 一样擅长穿越平面,并且需要一个更少的方向去做。

【讨论】:

    【解决方案4】:

    假设您在笛卡尔平面上有一组点 S,坐标为 (xi,yi),给定一个任意圆心为 (x c,yc) 和半径 r 你想找到该圆内包含的所有点。

    我还将假设点和圆可能会移动,因此某些可以加快速度的静态结构不一定是合适的。

    想到三件事可以加快速度:

    首先,您可以检查:

    (xi-xc)^2 + (yi-yc)^2 <= r^2
    

    而不是

    sqrt((xi-xc)^2 + (yi-yc)^2) <= r
    

    其次,您可以通过记住点只能在圆内满足以下条件来稍微剔除点列表:

    • xi 在 [xc-r,xc+r] 范围内;和
    • yi 在 [yc-r,yc+r] 范围内;和

    这被称为边界框。您可以将其用作近似值,也可以将点列表缩减为较小的子集,以便使用第一个等式进行准确检查。

    最后,按 x 或 y 顺序对点进行排序,然后您可以进行二等分搜索以找到可能在边界框内的点集,从而进一步减少不必要的检查。

    【讨论】:

    • 只有最后一个建议实际上改变了渐近运行时间。
    • 是的,但最后一个建议在其他两个上下文之外毫无意义。
    • 该算法的最坏情况运行时间仍然为 O(n)。
    • (假设所有点都具有相同的 y 值并且您选择按 y 排序)
    • 祝你好运,在最坏的情况下找到这个问题的 sub-O(n) 解决方案。
    【解决方案5】:

    我使用了 Andreas 的代码,但它包含一个错误。例如,我在平面 [13, 2], [13, -1] 上有两个点,我的原点是 [0, 0],半径为 100。它只找到 1 个点。这是我的解决方法:

    void findPoints(Node * root, vector<Node*> & result, Node * origin, double radius, int currAxis = 0) {
    if (root) {
        if (pow((root->coords[0] - origin->coords[0]), 2.0) + pow((root->coords[1] - origin->coords[1]), 2.0) < radius * radius) {
            result.push_back(root);
        }
    
        if (root->coords[currAxis] - origin->coords[currAxis] > radius) {
            findPoints(root->right, result, origin, radius, (currAxis + 1) % 2);
            return;
        }
        if (origin->coords[currAxis] - root->coords[currAxis] > radius) {
            findPoints(root->left, result, origin, radius, (currAxis + 1) % 2);
            return;
        }
        findPoints(root->right, result, origin, radius, (currAxis + 1) % 2);
        findPoints(root->left, result, origin, radius, (currAxis + 1) % 2);
    }
    }
    

    不同之处在于 Andreas 现在检查了带有不完整的 if (!root->greater) 的孩子。另一方面,我不做那个检查,我只是检查根是否有效。 如果您发现错误,请告诉我。

    【讨论】:

      【解决方案6】:

      根据您有多少预计算时间,您可以像这样构建一棵树:

      第一个节点分支是 x 值,在它们下面是 y 值节点,在它们下面是半径值节点。在每个叶子上都有一个点的哈希集。

      当您想计算 x,y,r 处的点时:遍历您的树并沿着与您的 x,y 值最接近的分支向下走。当您到达根级别时,您需要进行一些匹配(恒定时间的东西),但是您可以找到一个半径,使得该圆中的所有点(由树中的路径定义)都在指定的圆内由 x,y,r 和另一个圆(树中的 x_tree,y_tree 与以前相同,但 r_tree 不同)使得原始圆中的所有点(由 x,y,r 指定)都在该圆中。

      从那里,遍历两个树圈中较大的点中的所有点:如果一个点在较小的圈中,则将其添加到结果中,如果不是,则对其运行距离检查。

      唯一的问题是预先计算树需要很长时间。不过,您可以通过更改树中有多少 x、y 和 r 分支来指定要花费的时间。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-01-01
        • 1970-01-01
        • 2012-05-21
        • 1970-01-01
        • 2016-02-21
        • 1970-01-01
        • 2022-11-23
        • 1970-01-01
        相关资源
        最近更新 更多