确定一个点是否在多个矩形的并集中

5
我有一组由其左上角和右下角(所有坐标都为整数)定义的平行于坐标轴的二维矩形。给定一个点查询,你如何有效地确定它是否在其中一个矩形中?我只需要一个是/否的答案,不需要考虑它在哪个矩形中。
我可以通过检查(x, y)是否在((x1, y1), (x2, y2))中来判断它是否在矩形内,通过确认x是否在x1和x2之间以及y是否在y1和y2之间来实现。我可以单独对每个矩形执行此操作,这将在矩形数的线性时间内运行。但由于我有很多矩形,而且会进行大量的点查询,因此我希望有更快的方法。

@pablochan 添加到问题中。 - marshall
如果矩形的顶点的x和y坐标不大(即在10^3以内),则可以使用2D BIT完成。 - Fallen
@Fallen 它们最多为65535。 - marshall
@marshall,一个包含65535个简单if语句的循环大约需要1-10毫秒。虽然追求更好的算法是个好主意,但你能先确认这个时间对你来说是否太长吗? - Shahbaz
@Shahbaz - 坐标最多为65k,使位掩码查找变得不切实际(如果每个点使用一位,则为512M)。 - Floris
@pablochan 我将执行1千万到1亿次查询,而且我有大约1百万个矩形。 - marshall
4个回答

6
答案取决于你有多少个矩形。暴力方法会逐一检查你的坐标是否在每个矩形对中:
found = false
for each r in rectangles:
  if point.x > r.x1 && point.x < r.x2:
    if point.y > r.y1 && point.y < r.y2
      found = true
      break

你可以通过将矩形分类到区域中,并查看“边界矩形”来提高效率。然后,您可以通过逐渐减小的边界矩形树进行二进制搜索。这需要前期更多工作,但使查找为O(ln(n)),而不是O(n)——对于大量矩形和许多查找,性能改进将是显着的。您可以在此先前答案中看到一个版本(它查看矩形与一组矩形的交集-但您可以轻松适应“点在内部”)。更普遍地,查看四叉树的主题,这是您在处理2D问题时需要的数据结构。

稍微不那么有效(但更快)的方法将按左下角排序矩形(例如),然后您只需要搜索矩形的子集。

如果坐标是整数类型,您可以创建一个二进制掩码-然后查找就是单个操作(在您的情况下,这将需要512MB的查找表)。如果您的空间相对稀疏(即“未命中”的概率相当大),则可以考虑使用欠采样的位图(例如,使用坐标/8)-然后映射大小降至8M,如果您没有命中,则可以节省查看更仔细的费用。当然,您必须将左/底部舍入,并将顶部/右侧坐标舍入以使其正常工作。

举个例子:

假设坐标只能是x轴0-15和y轴0-7。有三个矩形(全部为[x1 y1 x2 y2]: [2 3 4 5][3 4 6 7][7 1 10 5])。我们可以在一个矩阵中绘制它们(我用矩形的编号标记左下角-请注意,1和2重叠):

...xxxx.........
...xxxx.........
..xxxxx.........
..x2xxxxxxx.....
..1xx..xxxx.....
.......xxxx.....
.......3xxx.....
................

你可以将其转化为由0和1组成的数组 - 这样“这个点是否有矩形”就等同于“这个位是否被设置”。一次查找就可以得到答案。为了节省空间,你可以对数组进行下采样 - 如果仍然没有命中,那么你就得到了答案,但如果有命中,你就需要检查“这是真实存在的” - 所以它节省的时间较少,并且节省的量取决于矩阵的稀疏程度(越稀疏速度越快)。下采样后的数组如下所示(2倍下采样):
.oxx....
.xxooo..
.oooxo..
...ooo..

我使用x来标记“如果您到达此点,则肯定在矩形内”,而使用o表示“其中一些是矩形”。现在许多点都是“可能的”,节省的时间更少。如果您进行更严格的下采样,可以考虑使用两位掩码:这将允许您说“整个块都填满了矩形”(即 - 不需要进一步处理:x上面),或者“需要进一步处理”(如上所述o)。这很快开始变得比Q树方法更复杂...

底线:您越是事先对矩形进行排序/组织,查找速度就越快。


如果使用二分查找,时间复杂度可达O(log(n)),这将是一个完美的解决方案。请问您能否更详细地解释一下整数掩码的思路? - marshall
@marshall - 更新了二进制算法的链接。 - Floris
如果每行有2^16位,而且有2^16行,那么我们不是会用掉2^32的空间吗? - marshall
@marshall - 是的。每个位置1比特,那就是2^24字节,或256 MB。很大,但不是不可能的。考虑到您所设想的矩形数量和查找次数,这可能是最有效的方法(但请注意,由于您的数组不会在高速缓存中,因此速度不会非常快)。 - Floris
抱歉,我可能有点迟钝,但为什么是2的24次方而不是2的32次方? - marshall
抱歉,我的大脑现在有点混乱。关键是 - 它是位与字节之间的选择。建议以“每个点一个比特”的方式存储信息。由于2^16在侧面,因此它不是2^32字节,而是2^32位。但仍然是2^29字节 - 因此是512 MB(兆字节)。这就是我最初提到的内容,然后我对自己产生了疑问,并说256(我不知道那个数字从哪里来)。请查看我今天早些时候发表的评论,就在您的问题下面。 - Floris

0

将矩形的坐标部分存储到树结构中。对于任何左值,都要创建一个条目,指向相应的右值、相应的顶部值和相应的底部值。

要进行搜索,您必须检查点的x值与左值的值。如果所有左值都不匹配,意味着它们大于您的x值,则知道该点在任何矩形外。否则,您将检查x值是否与相应左值的右值匹配。同样,如果所有右值都不匹配,则说明您在外面。否则,与顶部和底部值相同。一旦找到匹配的底部值,就知道您在任何矩形内部,并且完成了检查。

如我在下面的评论中所述,有很多优化的空间,例如最小的左值和顶部值以及最大的右值和底部值,可以快速检查是否在外面。

以下方法使用C#编写,并需要适应您偏好的语言:

public class RectangleUnion
{
    private readonly Dictionary<int, Dictionary<int, Dictionary<int, HashSet<int>>>> coordinates =
        new Dictionary<int, Dictionary<int, Dictionary<int, HashSet<int>>>>();

    public void Add(Rectangle rect)
    {
        Dictionary<int, Dictionary<int, HashSet<int>>> verticalMap;

        if (coordinates.TryGetValue(rect.Left, out verticalMap))
            AddVertical(rect, verticalMap);
        else
            coordinates.Add(rect.Left, CreateVerticalMap(rect));
    }

    public bool IsInUnion(Point point)
    {
        foreach (var left in coordinates)
        {
            if (point.X < left.Key) continue;

            foreach (var right in left.Value)
            {
                if (right.Key < point.X) continue;

                foreach (var top in right.Value)
                {
                    if (point.Y < top.Key) continue;

                    foreach (var bottom in top.Value)
                    {
                        if (point.Y > bottom) continue;

                        return true;
                    }
                }
            }
        }

        return false;
    }

    private static void AddVertical(Rectangle rect,
        IDictionary<int, Dictionary<int, HashSet<int>>> verticalMap)
    {
        Dictionary<int, HashSet<int>> bottomMap;
        if (verticalMap.TryGetValue(rect.Right, out bottomMap))
            AddBottom(rect, bottomMap);
        else
            verticalMap.Add(rect.Right, CreateBottomMap(rect));
    }

    private static void AddBottom(
        Rectangle rect,
        IDictionary<int, HashSet<int>> bottomMap)
    {
        HashSet<int> bottomList;
        if (bottomMap.TryGetValue(rect.Top, out bottomList))
            bottomList.Add(rect.Bottom);
        else
            bottomMap.Add(rect.Top, new HashSet<int> { rect.Bottom });
    }

    private static Dictionary<int, Dictionary<int, HashSet<int>>> CreateVerticalMap(
        Rectangle rect)
    {
        var bottomMap = CreateBottomMap(rect);
        return new Dictionary<int, Dictionary<int, HashSet<int>>>
                   {
                       { rect.Right, bottomMap }
                   };
    }

    private static Dictionary<int, HashSet<int>> CreateBottomMap(Rectangle rect)
    {
        var bottomList = new HashSet<int> { rect.Bottom };
        return new Dictionary<int, HashSet<int>>
                   {
                       { rect.Top, bottomList }
                   };
    }
}

这不是很美观,但应该能指引你朝正确的方向前进。


你能否用英语解释一下它在做什么吗?我不懂C#。 - marshall
你能解释一下这为什么是“快速”的吗? - Floris
很难用“英语”写出来,所以我用C#来表达,但你可以将其想象成一种伪代码。但我会尝试重新设计代码,使其更加自解释。该算法的速度在于坐标存储的Tee结构。我知道,可能会有许多缺点,比如任何矩形都会创建一个单一路径,或者点位于最右下角等等,但更有可能的是该点位于某个位置内部。这里有很大的优化空间。此方法仅在您想检查许多点是否与联合相交时才有用。 - abto

0

我最喜欢用于各种2D几何查询的算法是扫描线算法。它被广泛应用于CAD软件中,这可能是你的程序目的的猜测。

基本上,你需要按照X轴对所有点和多边形顶点(在你的情况下为所有4个矩形角)进行排序,并沿着X轴从一个点到下一个点前进。对于非曼哈顿几何图形,您还需要引入中间点,即线段交点。

数据结构是当前X位置处垂直线与点和多边形(矩形)边缘交点的平衡树,按Y方向排序。如果正确维护该结构,则可以很容易地确定当前X位置处的点是否包含在矩形内:只需检查相邻于该点的垂直边缘交点的Y方向。如果允许矩形重叠或具有矩形孔,则稍微复杂一些,但仍然非常快速。

N个点和M个矩形的总体复杂度为O((N + M)* log(N + M))。实际上可以证明这是渐近最优的。


0
通过整数(0..n-1)来标识你的矩形。 使用两个区间树。 一个保存每个矩形的dx(x_min,x_max):itvx。 另一个保存每个矩形的dy:itvy。 让我们称之为q的查询2D点。 用q.x查询itvx -> 返回一组整数(可能是命中的矩形) 用q.y查询itvy -> 返回另一组整数。 这两组整数的交集就是命中的矩形集合。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接