在一个轴对齐的矩形列表中,唯一报告交叉点

4

我有一组坐标,它们形成轴对齐的二维框(轴向/等向矩形,rects)。
我想知道有多少个盒子相互交错,而不会重复计数。例如,我有A、B和C三个盒子。如果A与B相交,B与A相交仍然只算作一个交叉而不是两个

我的思路是挑出一个盒子,与其余所有盒子进行比较,然后将其舍弃,因为比较已经完成,我不想重复。例如,回到A、B和C,如果我正在处理A并且它与B相交但不与C相交,那么我已经完成了A的比较,因此就不需要在循环中保留它。 因此,一旦我开始处理B,它就不会重新检查A。

我想不出更快的方法。这类似于使用线性搜索查找重复项,我认为最坏情况下的复杂度将是O(n^2)。我不知道如何使用二分搜索,因为可能会有多个交点。我还看了排序算法,但我需要一个不会再次回溯检查旧值的算法。


即使有交叉点,这显然是一个n^2的算法。 - hasan
1
所有的框是否与轴对齐? - Paddy3118
1
这些盒子有多少维? - kcsquared
1
盒子是轴对齐的二维盒子 @Paddy3118 - wasi hassan
2
高效地找到N个矩形的重叠部分 - greybeard
显示剩余6条评论
3个回答

2
你可以用 O(n log n) 的时间复杂度解决这个问题。由于你尝试在二维空间中解决一个静态交集问题,一个常见的技巧是将问题转化为一个一维动态交集问题(即,一个空间维度成为时间维度)。
假设你有一个左下角为 (x1, y1)、右上角为 (x2, y2) 的闭合矩形。这个矩形变成了两个“区间事件”:
Interval [y1, y2] inserted at time x1
Interval [y1, y2] removed after time x2

将所有矩形按时间转换为事件,并按时间排序(相同时插入在移除之前)。
现在,您需要一个数据结构,可以添加和删除区间[A,B],并且还可以查询与[A,B]相交的数据结构中的区间数量。然后,按排序顺序处理“区间事件”,但在插入每个区间[A,B]之前,保持当前区间与[A,B]相交的运行总和。
一种实现每个操作的O(log n)时间复杂度的数据结构是使用两个平衡二叉搜索树:一个保存区间起始点,另一个保存区间终点。您还需要增强每个BST成为顺序统计树,以快速查询树中小于或等于某个值的点数。
然后,找到当前数据结构中有多少个区间与任意区间[A,B]相交很简单;该计数为:
#(Intervals intersecting [A, B]) =

  #(values in the beginning-points tree that are <= B) 
- #(values in the ending-points tree that are < A)

你可以通过检查两个区间相交的定义来确认它是正确的:两个区间都没有在另一个区间结束之后才开始。
你也可以用一个前缀和数据结构(例如Fenwick tree)来替换顺序统计树,这样可以用更少的代码达到同样的复杂度。

这很聪明,但我不会得到重复的吗? - wasi hassan
@wasihassan 你说的doubles是什么意思? - kcsquared
比如说,它不会重复计算交叉点。例如,我有盒子A、B和C。如果A与B相交,B与A相交仍然只算作一个交叉点,而不是两个分开的交叉点。@kcsquared - wasi hassan
@wasihassan 感谢您的提问;不,它只会计算交叉点一次。解决方案最复杂的部分是编写一个顺序统计树,如果您在库中没有访问权限。 - kcsquared
1
抱歉再次打扰您,这可能是一个愚蠢的问题,但我该如何比较点,例如在起始点树中比较小于等于B的值?我是否必须将一个集合的x和y与另一个集合的x和y进行比较,并且只有当一个集合的两个值都大于另一个集合时,才能判断一个点是否大于另一个点?@kcsquared - wasi hassan
@wasihassan 不用担心,问任何问题都可以。最好展示实际的实现或详细的伪代码 - 我只有Python中的Order-statistic树代码,所以我将发布一些算法的伪代码。 - kcsquared

1

这是kcsquared算法的示例实现。由于您没有指定语言,我选择了Python,因为我熟悉它且代码简洁易读。矩形具有左下角坐标(x,y)和右上角坐标(X,Y)。

def intersections(rects):
    events = []
    for A in rects:
        events.append((A.x, 'insert', A.y, A.Y))
        events.append((A.X, 'remove', A.y, A.Y))
    intersections = 0
    ys = SortedList()
    Ys = SortedList()
    for x, op, y, Y in sorted(events):
        if op == 'insert':
            intersections += ys.bisect_right(Y) - Ys.bisect_left(y)
            ys.add(y)
            Ys.add(Y)
        else:
            ys.remove(y)
            Ys.remove(Y)
    return intersections

对5,000个随机矩形列表进行测试,与O(n2)参考实现进行比较,显示交点数量和运行时间:

124257  5465 ms  reference
124257   127 ms  intersections

121166  5444 ms  reference
121166   124 ms  intersections

118980  5435 ms  reference
118980   124 ms  intersections

使用10,000个矩形:

489617  22342 ms  reference
489617    292 ms  intersections

489346  22491 ms  reference
489346    296 ms  intersections

489990  22302 ms  reference
489990    290 ms  intersections

完整代码:

def reference(rects):
    intersections = 0
    for A, B in combinations(rects, 2):
        if A.X >= B.x and A.x <= B.X and A.Y >= B.y and A.y <= B.Y:
            intersections += 1
    return intersections

def intersections(rects):
    events = []
    for A in rects:
        events.append((A.x, 'insert', A.y, A.Y))
        events.append((A.X, 'remove', A.y, A.Y))
    intersections = 0
    ys = SortedList()
    Ys = SortedList()
    for x, op, y, Y in sorted(events):
        if op == 'insert':
            intersections += ys.bisect_right(Y) - Ys.bisect_left(y)
            ys.add(y)
            Ys.add(Y)
        else:
            ys.remove(y)
            Ys.remove(Y)
    return intersections

from random import randint, randrange
from itertools import combinations
from timeit import default_timer as timer
from sortedcontainers import SortedList
from collections import namedtuple

Rect = namedtuple('Rect', 'x X y Y')

for _ in range(3):
    rects = [Rect(x, x + randint(1, 100), y, y + randint(1, 100))
             for _ in range(5000)
             for x, y in [(randrange(1000), randrange(1000))]]
    for func in reference, intersections:
        t0 = timer()
        result = func(rects)
        t1 = timer()
        print(result, '%4d ms' % ((t1 - t0) * 1e3), func.__name__)
    print()

0

你可以在一定程度上减少平均复杂度,更准确地说是计算时间,但最坏情况下仍然会得到一个 的复杂度... 因为本质上这是一个 O(n²) 的算法,因为你必须逐个测试每个 N 个盒子。

减少计算时间的一种方法是“附加”每个盒子的外接球。这很容易计算:它的中心是盒子/矩形的8个顶点的重心,其半径是从该重心到一个顶点的任意距离。

诀窍是:给定两个盒子,如果它们的外接球不相交(两个中心之间的距离大于两个半径之和),则这两个盒子就不可能相交。 如果两个球相交,则这两个盒子可能相交。

这个技巧并没有改变复杂度,但它大大减少了计算时间,因为测试球体比测试盒子要快得多(特别是如果它们不平行于轴线...)。它还将减少列表大小,直到对任何潜在交点都为空。此外,每个盒子也只有一个缩短的(甚至是空的)潜在交点列表。

您可以通过使用盒子对列表进行精细测试、使用距离平方进行比较等方式来节省一些计算时间:虽然不是很多,但最终仍然可以节省一些时间。然后,您可以使用更少的盒子对计算实际的盒子交集。

这将显著提高与轴不对齐的许多3D随机盒子的性能,并且与轴对齐的少数2D矩形的性能则不会。在两种情况下,您始终会面临O(n²)的复杂度(即最坏情况)。


圆形的想法非常稳定,但是正方形和圆形之间的间隙会产生误报。而且很遗憾,它只能用于正方形。 - wasi hassan
@wasihassan 当然,你会得到假阳性...因为这是一种快速准则,可以快速消除_不可能_的交叉点,从而通过大大减少测试框的数量来加快计算时间。这与视频游戏中使用的精灵碰撞相同。而且,它适用于所有可能的凸体积。重心始终与每个边缘(定义)的距离相同,并且由于凸性,外接球的定义完全相同(也是定义)。它适用于2D、3D、4D等所有凸对象。 - Wisblade
@wasihassan,这仍然比毫无线索地测试所有框的交集要快得多,而不知道是否可能(或不可能)存在交集... - Wisblade
@老程序员 在3D中,有8个(问题在此时不清楚是2D还是3D)。对于“矩形”,那是一个打字错误,我想说的是“盒子”。 - Wisblade
通过编辑我明白了你的意思。如果我认为这个问题问得好,我就不会编辑它。 - greybeard
显示剩余3条评论

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