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

uniquely reporting intersections in a list of axis-aligned rectangles

我有一个坐标列表,这些坐标形成轴对齐的二维框(axis-oriented/iso-oriented 矩形,rects)。
我想看看有多少个盒子相互交叉而不重复。例如,我有盒子 A、B 和 C。如果 A 与 B 相交,B 与 A 相交仍将算作 one 交集,而不是 two 分开的。

我的想法是拿一个盒子,将其与其余所有盒子进行比较,然后将其扔掉,因为比较完成并且我不想重复。例如回到 A、B 和 C。如果我在 A 上并且它与 B 相交而不是与 C 相交,我已经完成了它的循环,因此不需要将它保留在循环中。因此,一旦我在 B 上,它就不会重新检查 A。

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

你可以稍微减少平均复杂度,更准确地说是减少计算时间,但你总是会遇到最坏的情况... 因为,从本质上讲,这是一个 O(n²) 算法,因为你必须每 N 个盒子两两测试。

减少计算时间的一种方法是将其外接球“附加”到每个框。计算起来很简单:它的中心是盒子的8个顶点的质心/矩形的4个顶点,它的半径是这个质心到一个顶点的任意距离。

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

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

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

这将是一个显着的提升,因为很多 3D 随机框未与轴对齐,而且很少有 2D 矩形与轴对齐。在这两种情况下,您总是会遇到 O(n²) 复杂性(即最坏的情况)。

您可以在 O(n log n) 中解决这个问题。由于您正在尝试解决二维的 static 交叉问题,因此一个常见的技巧是将问题转换为 dynamic 交叉问题维度(即,一个 space 维度成为时间维度)。

假设您有一个左下角 (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) 时间内执行此操作的一个数据结构是使用两个 balanced binary search trees: one holding beginning-points of intervals, and the other holding ending-points of intervals. You'll also need to augment each BST to be an Order Statistic Tree,以快速查询树中小于或等于特定值的点数。

然后,找出当前数据结构中有多少区间与任意区间相交 [A, B] 很简单;该计数是:

#(Intervals intersecting [A, B]) =

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

您可以根据两个相交区间的定义来检查它是否正确:两个区间都在另一个区间结束后开始。

您还可以将订单统计树替换为 prefix-sums 的数据结构,例如 Fenwick tree,对于相同的复杂性,它需要更少的代码。

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

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

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()