可以与单条直线相交的最大可能矩形数

Posted

技术标签:

【中文标题】可以与单条直线相交的最大可能矩形数【英文标题】:Maximum possible number of rectangles that can be crossed with a single straight line 【发布时间】:2018-08-26 07:13:24 【问题描述】:

我发现了这个挑战问题,说明如下:

假设在 XY 平面上有 n 个矩形。编写一个程序,计算在该平面上绘制的单条直线所能穿过的矩形的最大可能数量。

我已经头脑风暴了很长时间,但找不到任何解决方案。 也许在某个阶段,我们使用了动态编程步骤,但不知道如何开始。

【问题讨论】:

如何开始绘制从每个矩形角到另一个矩形角的线,然后选择最大值? @AndriyBerestovskyy 我们怎么知道这条线一定会穿过两个矩形的角? 为了使动态规划具有相关性,您需要以可以将其拆分为重叠子问题的方式来构建问题,并且可以使用这些子问题的最佳解决方案来生成最佳解决方案对于整个问题。我不知道这是否满足这个要求。 【参考方案1】:

(编辑我之前考虑旋转飞机的答案。)

这是O(n^2) 算法的草图,它结合了 Gassa 的想法和 Evgeny Kluev 将双线排列作为排序角度序列的参考。

我们从一个双向连接的边列表或类似结构开始,允许我们在O(1) 时间分割一条边,以及一种在填充二维平面时遍历我们创建的面的方法。为简单起见,我们只使用下面矩形十二个角中的三个:

9|     (5,9)___(7,9)
8|         |   |
7|    (4,6)|   |
6|    ___C |   |
5|   |   | |   |
4|   |___| |   |
3|  ___    |___|(7,3)
2| |   |  B (5,3)
1|A|___|(1,1)
 |_ _ _ _ _ _ _ _
   1 2 3 4 5 6 7

我们根据以下变换在对偶平面中插入三个点(角):

point p => line p* as a*p_x - p_y
line l as ax + b => point l* as (a, -b)

让我们按A, B, C的顺序输入分数。我们先输入A => y = x - 1。由于到目前为止只有一条边,我们插入B => y = 5x - 3,它创建了顶点(1/2, -1/2)并分割了我们的边。 (这个解决方案的一个优雅方面是双平面中的每个顶点(点)实际上是通过矩形角的线的双点。观察1 = 1/2*1 + 1/23 = 1/2*5 + 1/2,点(1,1)(5,3) .)

进入最后一点C => y = 4x - 6,我们现在寻找最左边的面(可能是一张不完整的面)相交的地方。这次搜索是O(n) 时间,因为我们必须尝试每张脸。我们找到并创建顶点(-3, -18),分割5x - 3 的下边缘并向上遍历边缘以在顶点(5/3, 2/3) 处分割x - 1 的右半部分。每次插入都有O(n) 时间,因为我们必须首先找到最左边的面,然后遍历每个面以分割边并标记顶点(线的交点)。

我们现在有双平面:

在构造线排列后,我们开始在三个示例点(矩形角)上进行迭代。重建与一个点相关的排序角度序列的部分魔法是将角度(每个角度对应于双平面中的有序线交点)划分为与右侧点对应的角度(具有更大的 x 坐标)和左边的那些并连接两个序列以获得从-90度到-270度的有序序列。 (右侧的点转换为相对于固定点具有正斜率的线;左侧的点具有负斜率。顺时针旋转您的服务/屏幕,直到(C*) 4x - 6 的线变为水平线,您会看到@ 987654343@ 现在有正斜率,A* 有负斜率。)

为什么有效?如果将原平面中的点p 转换为对偶平面中的线p*,则从左到右遍历该对偶线对应于在同样通过@ 的原始平面中绕p 旋转一条线987654348@。双线通过 x 坐标标记了这条旋转线的所有斜率,从负无穷(垂直)到零(水平)到无穷(再次垂直)。

(让我们总结一下矩形计数逻辑,在迭代角度序列的同时更新当前矩形的count_array:如果是1,则增加当前交叉点计数;如果是4并且线不是直接在一个角上,将其设置为 0 并减少当前的交叉点计数。)

Pick A, lookup A*
=> x - 1.

Obtain the concatenated sequence by traversing the edges in O(n)
=> [(B*) 5x - 3, (C*) 4x - 6] ++ [No points left of A]

Initialise an empty counter array, count_array of length n-1

Initialise a pointer, ptr, to track rectangle corners passed in
the opposite direction of the current vector.

Iterate:
  vertex (1/2, -1/2)
  => line y = 1/2x + 1/2 (AB)

  perform rectangle-count-logic

  if the slope is positive (1/2 is positive):
    while the point at ptr is higher than the line:
      perform rectangle-count-logic

  else if the slope is negative:
    while the point at ptr is lower than the line:
      perform rectangle-count-logic

  => ptr passes through the rest of the points up to the corner
     across from C, so intersection count is unchanged

  vertex (5/3, 2/3)
  => line y = 5/3x - 2/3 (AC)

我们可以看到(5,9) 位于通过AC (y = 5/3x - 2/3) 的线的上方,这意味着此时我们将计算与最右边矩形的交点,但尚未重置它的计数,这条线总共有 3 个矩形。

我们还可以在对偶平面的图中看到,其他角度序列:

for point B => B* => 5x - 3: [No points right of B] ++ [(C*) 4x - 6, (A*) x - 1]

for point C => C* => 4x - 6: [(B*) 5x - 3] ++ [(A*) x - 1]
(note that we start at -90 deg up to -270 deg)

【讨论】:

IMO 不能保证我们会以这种方式找到所有的交叉点。我们可以尝试 360 个不同的角度,或者我们可以尝试每 1/10 角度,或者每 1/100 等。所以算法会给出具有预定义精度的结果,但答案永远不会是精确的最大值......跨度> 我认为需要检查投影方向与连接投影上相邻点对的每条线之间的角度,并旋转最小的角度。 @n.m.你能解释一下吗?我不确定您所说的“投影方向”和“彼此相邻”的对是什么意思。也许您可以发布答案? 由于您旋转并始终在x上投影,因此投影方向将为y(旋转后)。彼此相邻意味着它们之间没有其他点。 @n.m.在我看来,“它们之间没有其他点”的“一对点”是同一点:)我还是不清楚,你能解释一下吗?【参考方案2】:

下面的算法怎么样:

RES = 0 // maximum number of intersections
CORNERS[] // all rectangles corners listed as (x, y) points

for A in CORNERS
    for B in CORNERS // optimization: starting from corner next to A
        RES = max(RES, CountIntersectionsWithLine(A.x, A.y, B.x, B.y))

return RES

换句话说,从每个矩形角开始画线到另一个矩形角,并找到最大的交点数。正如@weston 所建议的,我们可以通过从A 旁边的角落开始内循环来避免计算同一行两次。

【讨论】:

您至少可以避免两次计算同一行。 A-B B-A。您还可以通过保持最大值来避免内存复杂性。 @mnistic 您的示例仅在两个矩形的角之间绘制线条。答案中的建议是检查所有矩形角之间的线条。 所以,O(N^3) 复杂度?【参考方案3】:

解决方案

在图中所有线的空间中,经过一个角的线正是那些即将减少数量或交点的线。换句话说,它们各自形成一个局部最大值。

对于每条至少经过一个角的线,存在一条经过两个具有相同交叉点数的角的关联线。

结论是我们只需要检查由两个矩形角形成的线,因为它们形成了一个完全代表我们问题的局部最大值的集合。从那些我们选择交叉点最多的那个。

时间复杂度

此解决方案首先需要恢复通过两个角的所有线。这样的行数是O(n^2)

然后我们需要计算给定直线和矩形之间的交叉点数。这显然可以通过比较每个矩形在 O(n) 中完成。

可能有更有效的方法来进行,但我们知道这个算法最多O(n^3)

Python3 实现

这是该算法的 Python 实现。我更注重可读性而不是效率,但它完全符合上述定义。

def get_best_line(rectangles):
    """
    Given a set of rectangles, return a line which intersects the most rectangles.
    """

    # Recover all corners from all rectangles
    corners = set()
    for rectangle in rectangles:
        corners |= set(rectangle.corners)

    corners = list(corners)

    # Recover all lines passing by two corners
    lines = get_all_lines(corners)

    # Return the one which has the highest number of intersections with rectangles
    return max(
        ((line, count_intersections(rectangles, line)) for line in lines),
        key=lambda x: x[1])

此实现使用以下帮助程序。

def get_all_lines(points):
    """
    Return a generator providing all lines generated
    by a combination of two points out of 'points'
    """
    for i in range(len(points)):
        for j in range(i, len(points)):
            yield Line(points[i], points[j])

def count_intersections(rectangles, line):
    """
    Return the number of intersections with rectangles
    """
    count = 0

    for rectangle in rectangles:
        if line in rectangle:
           count += 1

    return count

这里是用作矩形和线条数据结构的类定义。

import itertools
from decimal import Decimal

class Rectangle:
    def __init__(self, x_range, y_range):
        """
        a rectangle is defined as a range in x and a range in y.
        By example, the rectangle (0, 0), (0, 1), (1, 0), (1, 1) is given by
        Rectangle((0, 1), (0, 1))
        """
        self.x_range = sorted(x_range)
        self.y_range = sorted(y_range)

    def __contains__(self, line):
        """
        Return whether 'line' intersects the rectangle.
        To do so we check if the line intersects one of the diagonals of the rectangle
        """
        c1, c2, c3, c4 = self.corners

        x1 = line.intersect(Line(c1, c4))
        x2 = line.intersect(Line(c2, c3))

        if x1 is True or x2 is True \
                or x1 is not None and self.x_range[0] <= x1 <= self.x_range[1] \
                or x2 is not None and self.x_range[0] <= x2 <= self.x_range[1]:

            return True

        else:
            return False

    @property
    def corners(self):
        """Return the corners of the rectangle sorted in dictionary order"""
        return sorted(itertools.product(self.x_range, self.y_range))


class Line:
    def __init__(self, point1, point2):
        """A line is defined by two points in the graph"""
        x1, y1 = Decimal(point1[0]), Decimal(point1[1])
        x2, y2 = Decimal(point2[0]), Decimal(point2[1])
        self.point1 = (x1, y1)
        self.point2 = (x2, y2)

    def __str__(self):
        """Allows to print the equation of the line"""
        if self.slope == float('inf'):
            return "y = ".format(self.point1[0])

        else:
            return "y =  * x + ".format(round(self.slope, 2), round(self.origin, 2))

    @property
    def slope(self):
        """Return the slope of the line, returning inf if it is a vertical line"""
        x1, y1, x2, y2 = *self.point1, *self.point2

        return (y2 - y1) / (x2 - x1) if x1 != x2 else float('inf')

    @property
    def origin(self):
        """Return the origin of the line, returning None if it is a vertical line"""
        x, y = self.point1

        return y - x * self.slope if self.slope != float('inf') else None

    def intersect(self, other):
        """
        Checks if two lines intersect.
        Case where they intersect: return the x coordinate of the intersection
        Case where they do not intersect: return None
        Case where they are superposed: return True
        """

        if self.slope == other.slope:

            if self.origin != other.origin:
                return None

            else:
                return True

        elif self.slope == float('inf'):
            return self.point1[0]

        elif other.slope == float('inf'):
            return other.point1[0]

        elif self.slope == 0:
            return other.slope * self.origin + other.origin

        elif other.slope == 0:
            return self.slope * other.origin + self.origin

        else:
            return (other.origin - self.origin) / (self.slope - other.slope)

示例

这是上述代码的一个工作示例。

rectangles = [
    Rectangle([0.5, 1], [0, 1]),
    Rectangle([0, 1], [1, 2]),
    Rectangle([0, 1], [2, 3]),
    Rectangle([2, 4], [2, 3]),
]

# Which represents the following rectangles (not quite to scale)
#
#  *
#  *   
#
# **     **
# **     **
#
# **
# **

我们可以清楚地看到,一个最优解应该找到一条经过三个矩形的直线,而这确实是它输出的内容。

print(' with  intersections'.format(*get_best_line(rectangles)))
# prints: y = 0.50 * x + -5.00 with 3 intersections

【讨论】:

这是一个简单的蛮力解决方案。如果这是可以接受的,那么这个问题可能就不会被称为挑战 如果我找到更好的方法,我会改进它,我只是还没有。有什么建议吗?另外,它不是蛮力,因为它确实将问题简化为线性函数空间的一个子集。 我认为有更好的方法,但绝对不容易。我还没有完全确定它。它涉及将所有矩形投影在一条线上,旋转该线,并计算每个角度的间隔重叠。诀窍是如何有效地从一个旋转角度转到下一个旋转角度,而无需重新计算所有内容。 我已经试过了。事实证明,找到投影相当于以给定角度投影线上的每个点。然后你想要做的是找到临界角并计算那里的投影,但事实证明这些临界角是由角之间的角度定义的。所以这个解决方案等同于我提供的解决方案,但在我看来并不那么可读。另外,我不相信你可以不从相邻的投影重新计算投影,因为投影不是单射的。您正在丢失投影中的信息。 @n.m.和 Olivier,我添加了一个 O(n^2 (log n + m)) 答案。你怎么看?【参考方案4】:

如果考虑角度为 Θ 的旋转线,并且将所有矩形投影到这条线上,则得到 N 条线段。通过增加横坐标并对端点进行排序并保持从左到右遇到的间隔计数(跟踪端点是开始还是结束),很容易获得与这条线垂直的矩形的最大数量。这以绿色显示。

现在两个矩形被所有线以两个内切线之间的角度相交[红色示例],因此要考虑所有“事件”角度(即可以观察到计数变化的所有角度) 是这些 N(N-1) 个角度。

那么暴力破解方案就是

对于所有的极限角(O(N²)),

将矩形投影到旋转线上(O(N) 次操作),

计算重叠并保留最大的(O(N Log N) 进行排序,然后 O(N) 进行计数)。

这总共需要 O(N³Log N) 次操作。

假设如果我们可以增量地进行排序,则不需要对每个角度都重新进行全部排序,我们可以希望将复杂度降低到 O(N³)。这需要检查。


注意:

限制线条通过一个矩形角的解决方案是错误的。如果你从一个矩形的四个角到另一个矩形的整个范围绘制楔形,即使有一条穿过它们三个的线,也会有空白空间可以放置一个不会被触及的整个矩形。

【讨论】:

添加了O(n^2 (log n + m)) 答案。你怎么看? @גלעדברקן:只考虑通过其中一个角落的线条可能会错过更好的解决方案。而且您没有为复杂性提供任何理由。 首先,(我们不考虑线,我们正在考虑弧,并且)任何作为解决方案且不通过任何角的线都可以稍微旋转以接触角。其次,考虑了复杂性,4 个角 * n 个矩形 * 2 * (log n + m) 用于在区间树中的每次搜索和插入。 @גלעדברקן:我们确实考虑了线条,“稍微旋转”会导致一些交叉点消失。你甚至没有定义 m。 你能想出一个不能旋转到一个角落的解决方案线的例子吗?这没有道理。如果一条线没有接触任何角,则旋转它直到它接触到的第一个角。根据定义,这样的运动将保留所有当前的交叉点。【参考方案5】:

我们可以有一个 O(n^2 (log n + m)) 动态规划方法,通过调整 Andriy Berestovskyy 的想法,即稍微迭代角以将当前角与所有其他矩形的关系插入到每个 @987654324 的间隔树中@ 迭代周期。

将为我们正在尝试的角落创建一棵新树。对于每个矩形的四个角,我们将遍历其他每个矩形。我们将插入的是标记成对矩形的最远角相对于当前固定角创建的弧的角度。

在下面的示例中,对于固定的下矩形的角R,在插入中间矩形的记录时,我们将插入标记从p2p1R 相关的弧的角度(关于(37 deg, 58 deg))。然后当我们检查与R相关的高矩形时,我们将插入标记弧的角度区间,从p4p3R相关(大约(50 deg, 62 deg))。

当我们插入下一条弧线记录时,我们将对照所有相交区间对其进行检查,并保留最多相交的记录。

(请注意,由于我们的目的是 360 度圆上的任何弧都有对应的旋转 180 度,因此我们可能需要进行任意截断(欢迎任何替代见解)。例如,这意味着来自45 度到 315 度会分成两部分:[0, 45] 和 [135, 180]。任何非分割弧只能与其中一个相交,但无论哪种方式,我们都可能需要一个额外的哈希来确保矩形是没有重复计算。)

【讨论】:

你能提供伪代码吗?这个算法有我可以搜索的名称吗? @OlivierMelançon 当然,我会在明天或周末添加一些伪代码。我不知道它是否有一个可搜索的名称。 很高兴听到这个消息,我相信我知道您在做什么,但我希望看到它起作用。谢谢! @OlivierMelançon 我想我可能会继续添加伪代码,因为 Gassa 提供了一个更优雅的解决方案,并且有一些相似之处。【参考方案6】:

这是一个 O(n^2 log n) 解决方案的草图。

首先,预赛与其他答案共享。 当我们有一条线穿过一些矩形时,我们可以将它平移到任何两侧,直到它穿过某个矩形的一个角。 之后,我们将该角固定为旋转中心,并将线旋转到两侧中的任何一个,直到它穿过另一个角。 在整个过程中,我们的直线和矩形边之间的所有交点都在这些边上,所以交叉点的数量保持不变,直线穿过的矩形数量也是如此。 因此,我们可以只考虑通过两个矩形角的线,其上限为 O(n^2),与任意线的无限空间相比,这是一个值得欢迎的改进。

那么,我们如何有效地检查所有这些行? 首先,让我们有一个外循环,它固定一个点 A,然后考虑所有通过 A 的线。 A有O(n)个选择。

现在,我们已经固定了一个点 A,并且想要考虑所有通过所有其他角 B 的线 AB。 为了做到这一点,首先根据 AB 的极角,或者换句话说,轴 Ox 和向量 AB 之间的角度对所有其他角 B 进行排序。 角度是从 -PI 到 +PI 或从 0 到 2 PI 或以其他方式测量的,我们切割圆以对角度进行排序的点可以是任意的。 排序在 O(n log n) 内完成。

现在,我们有点 B1, B2, ..., Bk 按 A 点周围的极角排序 (它们的数字 k 类似于 4n-4,所有矩形的所有角,除了点 A 是角的那个)。 首先,查看线 AB1 并计算该线在 O(n) 中穿过的矩形数量。 之后,考虑将 AB1 旋转到 AB2,然后 AB2 到 AB3,一直到ABk。 轮换期间发生的事件如下:

当我们旋转到 ABi,并且 Bi 是我们顺序中某个矩形的第一个角时,交叉的矩形数量增加 1旋转线一碰到 Bi

当我们旋转到 ABj,并且 Bj 是我们顺序中某个矩形的最后一个角时,交叉的矩形数量减少 1线一转过 Bj.

在排序之后,但在考虑有序事件之前,可以通过一些 O(n) 预处理来确定哪些角是第一个和最后一个。

简而言之,我们可以旋转到下一个此类事件并更新在 O(1) 中交叉的矩形数量。 总共有 k = O(n) 个事件。 剩下要做的是在整个算法中跟踪这个数量的全局最大值。 答案就是这个最大值。

整个算法的运行时间为 O(n * (n log n + n + n)),也就是 O(n^2 log n),正如宣传的那样。

【讨论】:

不错的解决方案!这与我的想法一致,但解决起来更加优雅。 如果我们使用“排列”对角度序列进行排序,时间复杂度可能会降低到 O(n^2)(如 here 解释的那样)。 @EvgenyKluev 看起来不错,谢谢指点!但是我必须注意,O(n^2) 时间算法所需的 O(n^2) 内存实际上可能是不可行的,或者至少足够慢,以至于它的性能不会比 O(n^2 log n ) 时间解,只有 O(n) 内存。 太酷了!你能分享伪代码,只是为了好玩吗?我会等到最后,因为@EvgenyKluev 指出存在 O(n^2) 算法,但这绝对是当时的最佳答案。 @OlivierMelançon 我感觉代码不会增加太多,因为文本已经很相似了。另一方面,真实代码可能有太多细节掩盖了主流,比如处理位于彼此内部的矩形(如果点 A 完全在矩形 R 内,那么 R 不应该贡献任何角序列 B),所以我也不确定它是否会是一个有用的贡献。

以上是关于可以与单条直线相交的最大可能矩形数的主要内容,如果未能解决你的问题,请参考以下文章

使用Python判断线段是不是与矩形相交

n个矩形的交点 - 恰好是k个矩形相交的区域的最大数量

如何找到直线和矩形的交点?

C++编程,求俩矩形重叠面积的代码

POJ 2318 TOYS 叉积

leetcode 223