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

时间:2018-03-17 08:21:03

标签: algorithm geometry dynamic-programming computational-geometry

我发现了这个挑战问题,其中说明了以下内容:

  

假设XY平面上有n个矩形。编写一个程序来计算可以与在这个平面上绘制的单条直线交叉的最大可能矩形数。

see image for an example

我一直在集思广益,但无法找到任何解决方案。 也许在某个阶段,我们使用动态编程步骤,但无法弄清楚如何开始。

6 个答案:

答案 0 :(得分:8)

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

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

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

现在,我们有一个点A固定,并且想要考虑所有其他角落的AB线。 为此,首先根据AB的极角对所有其他角B进行排序,或者换句话说,对应于轴Ox和矢量AB之间的角度。 角度是从-PI到+ PI或从0到2 PI测量的,否则,我们将圆切割成分类角度的点可以是任意的。 排序在O(n log n)中完成。

现在,我们有点B 1 ,B 2 ,...,B k 按点A的极角分类(它们的数字k类似于4n-4,所有矩形的所有角落,除了点A是角落的那个角落)。 首先,查看AB 1 行,并计算该行在O(n)中越过的矩形数。 之后,考虑将AB 1 旋转到AB 2 ,然后将AB 2 旋转到AB 3 ,一直到AB <子>ķ。 轮换期间发生的事件如下:

  • 当我们旋转到AB i ,并且B i 是我们顺序中某个矩形的第一个角时,交叉的矩形数增加1一旦旋转线击中B i

  • 当我们旋转到AB j ,并且B j 是我们顺序中某个矩形的最后一个角时,交叉的矩形数减少1一旦该线旋转超过B j

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

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

整个算法在O(n *(n log n + n + n))中运行,即O(n ^ 2 log n),正如所宣传的那样。

答案 1 :(得分:4)

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

这是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,1)。)

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

在双平面中,我们现在有:

enter image description here

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

为什么会这样?如果原始平面中的点A*在双平面中转换为线p,则从左到右遍历该双线对应于在原始位置围绕p*旋转一条线也通过p的飞机。双线将此旋转线的所有斜率标记为x坐标,从负无穷大(垂直)到零(水平)到无穷大(再次垂直)。

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

p

我们可以看到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)的线之上,这意味着此时我们将计算与最右边的矩形的交点并且尚未重置它的计数,总计3个矩形这条线。

我们还可以在双平面图中看到其他角度序列:

AC (y = 5/3x - 2/3)

答案 2 :(得分:4)

解决方案

在图表中所有线条的空间中,经过一个角落的线条恰好是数量或交叉点即将减少的线条。换句话说,它们各自构成一个局部最大值。

对于经过至少一个角落的每条线,都存在一条相关的线,它通过两个交叉点数相同的角落。

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

时间复杂度

此解决方案首先需要恢复通过两个角落的所有行。此类行的数量为 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

答案 3 :(得分:3)

以下算法如何:

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旁边的角落开始内循环来避免两次计算相同的行。

答案 4 :(得分:2)

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

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

然后蛮力解决方案

  • 所有极限角(O(N²)),

    • 投影旋转线上的矩形(O(N)操作),

    • 计算重叠并保持最大值(O(N Log N)排序,然后O(N)计数)。

这需要总O(N³LogN)操作。

enter image description here

如果我们可以逐步完成各种角度的假设,我们可以希望将复杂性降低到O(N³)。这需要进行检查。

注意:

限制线条通过一个矩形的角落的解决方案是错误的。如果你从一个矩形的四个角到另一个矩形的整个范围绘制楔形,那么将保留一个空的空间,其中可以存在一个不会被触摸的整个矩形,即使它们中的三个存在一条直线。 / p>

enter image description here

答案 5 :(得分:1)

我们可以采用O(n^2 (log n + m))动态编程方法,通过调整Andriy Berestovskyy的想法,稍微迭代角落,将当前角落与所有其他矩形的关系插入到间隔树中对于我们的每个4n迭代周期。

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

在下面的示例中,对于插入中间矩形的记录,对于固定的下方矩形的角R,我们将插入标记弧的角度从p2到{{ 1}}与p1(约R)有关。然后,当我们检查与(37 deg, 58 deg)相关的高矩形时,我们将Rp4相对于p3插入标记弧的角度间隔(约R)。

当我们插入下一个圆弧记录时,我们会针对所有相交的间隔进行检查,并记录大多数交叉点。

enter image description here

(请注意,因为为了我们的目的,360度圆上的任何圆弧都有一个旋转180度的对应物,我们可能需要进行任意截止(任何其他见解都是受欢迎的)。例如,这意味着来自45度到315度将分成两个:[0,45]和[135,180]。任何非分裂弧只能与一个或另一个相交,但不管怎样,我们可能需要一个额外的哈希来确保矩形是没有重复计算。)