计算矩形中的点数

时间:2013-07-03 13:04:30

标签: algorithm

我在2D中有很多(数十亿)点我可以预处理,我想回答以下形式的查询:

给定矩形的所有四个角,输出矩形内的点数。

矩形可以处于任何方向(意味着矩形的轴可以以任何角度定向,而不仅仅是水平或垂直)。

对此有一个快速实用的算法吗?

更新。是否有任何数据结构存储点,这些点可以使查询可以在次线性时间内进行?

更新II 似乎答案是公司没有https://cstheory.stackexchange.com/questions/18293/can-we-perform-an-n-d-range-search-over-an-arbitrary-box-without-resorting-to-si。在任何情况下都接受最流行的答案。

9 个答案:

答案 0 :(得分:17)

将点表示为k-d tree

也就是说,一个二叉树,其中每个节点代表一个点,每个非叶节点可以被认为是在该节点的x或y值上垂直或水平(在每个级别上交替)分割当前区域

然后,进行查询:

  1. 当前节点=根

  2. 当前区域=当前节点的区域(可以在向下递归树时跟踪/计算)

  3. 如果当前区域完全包含在矩形内,请添加此节点作为子节点的点数(当前节点为+1)。

  4. 如果当前区域完全位于矩形内部,则不执行任何操作。

  5. 如果当前区域部分包含在矩形内:

    • 添加当前节点的点,如果它包含在矩形中。
    • 对两个孩子重复2.
  6. 计算一个区域或点是否包含在矩形中应该足够简单。

    每个查询应该在随机数据上平均花费O(log n)时间。

    虽然存在pathological个案例需要花费O(n)时间(即你需要探索整个/大部分树的地方)。这种情况的一个可能的例子是当大多数点围绕(非轴对齐)矩形(内部或外部)的边缘时,意味着上面提到的“不做任何事”部分很少适用。

答案 1 :(得分:10)

旧答案(如果您无法提前预处理点数):

  • 在包含矩形的方向上刻字矩形,其边/边朝向xy轴
  • 快速排除其外的所有要点
  • 使用此处说明的原则:How to determine if a point is in a 2D triangle?与矩形的四边/边(注意:,因为您总是使用相同的矩形来检查所有点,您可以预先 - 计算一些值)

你可以通过快速包含留在内接矩形中的边,边/面朝向xy轴的点来获得一些东西(不是很多,这取决于矩形的方向)。这需要一些预先计算,但考虑到你有很多分数这一点可以忽略不计。

新答案:

  • rect是我们的输入矩形
  • 假设您有f1(point,rect)检查点是否在矩形内。你可以使用我上面提到的那个。
  • 假设您有f2(shape,rect),它可以说明形状是否完全包含在rect中,或者rect是否完全包含在形状中,或者该形状与rect相交或完全不相交
  • 形状将是具有一定数量边的多边形(不高或与n成比例,因此我们可以假设f2O(1)),或者2D平面中的区域由2个边并延伸到无限(例如,由xy轴的正截面限定的区域)
  • 假设您有足够的时间预处理这些点,但不是无限的。让我们说我们可以买得起O(n * log(n))算法

我们想要获得的是一种在运行时调用f1和f2最少时间的算法。例如,与log(n)

的(相同顺序)成比例的东西

因此我们希望将我们的2D平面划分为m个形状,每个形状包含p个点。在运行时,我们用f2检查每个形状,我们可以有4个案例:

  1. 矩形完全包含在形状中:使用f1计数 此形状中包含的所有点都位于矩形中 (O(p) ),我结束了。
  2. 形状完全包含在 矩形:我向累加器添加了整个点数 包含在形状中。 (O(1) )
  3. 矩形和形状没有 相交:我跳过这个形状。
  4. 矩形和形状相交: 使用f1我计算这个形状所包含的所有点 矩形(O(p) ),我继续。
  5. 我们可以很幸运并且在案例1中迅速放弃,但通常我们必须检查所有形状,并且至少其中一个我们将必须检查所包含的所有点。所以这个算法是O(p) + O(m)。考虑到p * m = n,如果我们选择p = m = sqrt(n),我们会获得O(sqrt(n) ),这是我们用此方法获得的最佳效果。 (注意:我们执行f1多少次?这个数字实际上取决于矩形的形状,所以如果例如矩形非常长,它将与许多区域相交,导致许多调用f1。但是,我认为我们可以假设矩形的度量与nsqrt(n)甚至log(n)的顺序不同:n是巨大的。 )

    我们可以从这里提升;例如,我们可以说我们在形状之间有相邻性,第一次在形状和矩形之间找到重叠时,我只检查连续的形状。但是,我们必须检查的平均形状数量大约为p / 2和O(p/2) = (O(p) )。所以没有有效的收获。

    真正的好处是,如果我们在形状中加入一些层次结构。

    首先,我检查所有点,并找到我的界限值max_x,max_y,min_x,min_y。 (让我们假设这些边界是>> n。如果我们可以有关于点分布的先验,我们可以针对的优化将完全不同) 我们将每个包含(周围)log(n)点的形状划分为空间。我们首先使用xy轴将2D平面划分为4个形状(我也可以根据我的边界值居中)。这将是我们颠倒金字塔的第一层。 循环:对于包含多个log(n)点的每个区域,我们使用垂直或水平线(我们交替)将区域分成两半。如果一个边界设置为无限,分成两半,我使用相应的边界值。分割的每个区域都包含指针到分割它的区域。新区域是金字塔的第二层。我一直在分开,直到我的所有区域都包含(约)log(n)个点。当一个区域被拆分时,它包含指向" children"区域。我建造了我的金字塔。倒置金字塔的顶层包含n / log(n)形状,这是非常大的,但它并不重要:重要的是我们有log(n)金字塔等级。注意:对于每个级别的每个形状,我们知道它包含多少个点。注2:这个预先详细分析每个金字塔等级每个点平均一次,因此其复杂度为O(n *(log(n))。

    当我在输入中获得矩形时,我使用f2检查第一级的形状。

    1. 矩形完全包含在形状中:我输入此区域的子形状(如果有),否则我使用f1计算矩形内的点(O(log(n)))我丢弃任何其他形状。
    2. 形状完全包含在矩形中:我将累积器中包含的整个点数添加到我的累加器中。需要O(1)
    3. 矩形和形状不相交:我跳过这个形状。
    4. 矩形和形状相交:我输入此区域的子形状(如果有),否则我使用f1计算矩形(O(log(n) )内的点。
    5. 现在困难的部分:我们访问了多少种形状?同样,这取决于矩形的形状,它接触的形状数量。但是,对于每个级别,我们将访问多个不依赖于n的形状,因此访问的形状数量与O(log(n) )成比例。

      由于n非常大,我们可以假设与矩形边相交的形状数(导致对f1的昂贵调用)远小于O(log(n) )。整个算法应为O(log(n) )

      还有其他优化方法,但任何事情都会保持平均O(log(n) )

      最后注意:我们划分空间的方式必须是控制多边形所具有的边数,因为如果形状可以有大量的边,则某种程度上取决于点的数量(根据函数)我们称之为g),f2将为O(g(n) ),其复杂性必须再次倍增取决于n,我们的形状数量必须检查,所以可能不好。

答案 2 :(得分:3)

您需要的是某种二进制空间分区数据结构。这将为您提供一个候选人列表,您可以在其中进行真正的“多边形点”测试。

我建议你确保这是你应该自己编码的东西。例如,许多DB都内置了这种功能。您的数据实际上是否存在于数据库中?可以吗? (重新发明轮子没有意义......)

您可以在此处看到Point in Polygon问题的完美答案:How can I determine whether a 2D Point is within a Polygon?

答案 3 :(得分:3)

我建议您找一个可以应用于空间的旋转+移位变换,这样矩形的一个角就在(0,0)中,两个边沿着x和{{1轴。

现在,您将完成这些操作,应用相同的转换,然后检查y0 < x < rectangle_max_x

答案 4 :(得分:1)

制作三角形。假设,abcd是矩形,x是点,那么如果area(abx)+area(bcx)+area(cdx)+area(dax) equals area(abcd)则点在其中。

答案 5 :(得分:0)

您可以对矩形使用BoundBox,然后如果某个点位于boundbox内,您可以检查它是否与矩形碰撞,或者您可以使用定向有界框。

这是最简单的方法,不需要使用复杂的数据结构

答案 6 :(得分:0)

如果速度是一个问题,但内存/磁盘空间不是,请考虑执行以下操作,这应该是最有效的方法。

通过这种方式,您可以在进行任何重要数学运算之前执行一些非常快速的测试:

public class DataPoint
{
    double X, Y;
    ...
}
public bool IsInBoundingBox(Point p1, Point p2, Point p3, Point p4)
{
    // assume p1, p2, p3, p4 to be sorted
    return (X>p1.X && X<p3.X && Y>p4.Y && Y<p2.Y);
}

那么实际工作的顺序应该是......

// sort points of QueryRectangle: p1 is left-most, p2 is top-most, p3 is right-
// most, and p4 to be bottom-most; if there is a tie for left-most, p1 should
// be the bottom-left corner, p2 the top-left corner, p3 the top-right corner,
// and p4 the bottom-right corner

// See if the QueryRectangle in question is aligned with the grid; if it is,
// then the set of DataPoints that lie within the BoundingBox are within the
// QueryRectangle and no further calculation is needed

if (p1.X == p2.X || p1.X == p3.X || p1.X == p4.X) 
{
    // is orthogonal (aligned with axes)
    foreach(DataPoint dp in listDataPoints)
        if(dp.IsInBoundingBox())
        {
            // dp is in QueryRectangle; perform work
        }
}
else
{   
    foreach(DataPoint dp in listDataPoints)
        if(dp.IsInBoundingBox())
        {
            // perform further testing to see if dp is in QueryRectangle
        }
}

或者,如果你想使用旋转/翻译解决方案,就像viraptor建议的那样......

// sort points of QueryRectangle: p1 is left-most, p2 is top-most, p3 is right-
// most, and p4 to be bottom-most; if there is a tie for left-most, p1 should
// be the bottom-left corner, p2 the top-left corner, p3 the top-right corner,
// and p4 the bottom-right corner

public class DataPointList : List<DataPoint>
{
    public List<DataPoint> GetPointsInRectangle(Point p1, Point p2, Point p3, Point p4)
    {
        List<DataPoint> tempListDataPoints = new List<DataPoint>();
        foreach(DataPoint dp in this)
            if(dp.IsInBoundingBox()) tempListDataPoints.Add(dp);

        if (!(p1.X == p2.X || p1.X == p3.X || p1.X == p4.X))
        {
            // needs transformation
            tempListDataPoints.TranslateAll(-1 * p1.X, -1 * p1.Y);
            tempListDataPoints.RotateAll(/* someAngle derived from the arctan of p1 and p2 */);
            // Note: you should be rotating counter-clockwise by some angle >0 but <90

            // the new p1 will be 0,0, but p2, p3, and p4 all need to undergo the same transformations
            // transP1 = new Point(0,0);
            // transP2 = new Point(p2.Translate(-1 * p1.X, -1 * p1.Y).Rotate(/* someAngle derived from the arctan of p1 and p2 */));
            // transP3 = ...; transP4 = ...;

            foreach(DataPoint dp in tempListDataPoints)
                if (!(dp.X>transP1.X && dp.X<transP3.X && dp.Y>transP1.Y && dp.Y<transP3.Y)) tempListDataPoints.Remove(dp);
        }
        else
        {
            // QueryRectangle is aligned with axes, all points in bounding box
            // lie within the QueryRectangle, no need for transformation or any
            // further calculation

            // no code needs to go here, but you may want to keep it around for notes
        }

        return tempListDataPoints;
    }
}

或者,您可以使用数组执行上述代码。我会留下你的意思......

免责声明:我昨晚睡了2个小时,所以我不打算校对。您可能需要做一些小修复。或主要的。谁知道。 :)

答案 7 :(得分:0)

您可以简化解决问题的方法是找到包含给定( R )的最小轴对齐矩形( S )。使用一些空间树结构作为k-d树来查找 S 中包含的点的子集,最后,为该子集选择 R 内的点。

这种方法比@Dukelin提出的方法更容易实现,其中使用 R 直接执行k-d树搜索。

答案 8 :(得分:0)

我首先要沿任意轴预先计算点数组(让它为x)。然后在矩形的边界框x投影中对其进行二维搜索以获得具有x投影的点。这将减少要大幅检查的点数。

然后我们可以通过检查它们是否在矩形边界框中来进一步过滤这些点。但是,是的,这将是线性的。

然后我们可以为矩形采用变换矩阵(我假设我们已经有了它)。 Rectangle是奇异2-cube的仿射变换,因此我们可以在不计算实际逆矩阵的情况下找到逆变换。

对于

的直接变换矩阵
A D a
B E b
C F c

解决方案是:

d = 1/(AE − BD)

A' = Ed
B' = −Bd
C' = (BF − EC)d

D' = −Dd
E' = Ad
F' = (DC − AF)d

a' = b' = 0
c' = 1

然后,通过对每个点应用逆变换,我们将其转换为单个立方体,如果它最初位于矩形中,则为(0, 1)x(0, 1),否则将其转换为。{/ p >

UPD:或者,您可以执行以下操作,而不是所有转换内容:

让矩形点为P1..P4,并检查A

i = 1..4PAi计算为Pi - A

现在(Pi.x, Pi.y, 0)x(Pj.x, Pj.y, 0)的叉积将测量由A和相应的矩形边缘构成的三角形。并且,由于原始点都位于xy平面上,因此结果将类似于(0, 0, Sij),其中Sij是三角形的有符号平方。只需计算总和:

|(P1.x, P1.y, 0)x(P2.x, P2.y, 0)[3]|+
|(P2.x, P2.y, 0)x(P3.x, P3.y, 0)[3]|+
|(P3.x, P3.y, 0)x(P4.x, P4.y, 0)[3]|+
|(P4.x, P4.y, 0)x(P1.x, P1.y, 0)[3]|

并将其与矩形方块进行比较。如果它或多或少相等,那么该点在矩形中。会有一些小的计算错误,所以完全相同是不可能的。