33

I found this challenge problem which states the following :

Suppose that there are n rectangles on the XY plane. Write a program to calculate the maximum possible number of rectangles that can be crossed with a single straight line drawn on this plane.

see image for an example

I have been brainstorming for quite a time but couldn't find any solution. Maybe at some stage, we use dynamic programming steps but couldn't figure out how to start.

meowgoesthedog
  • 14,670
  • 4
  • 27
  • 40
Tapan Vaishnav
  • 447
  • 5
  • 11
  • How about to start drawing those lines from each rectangle corner to each other rectangle corner and then just choose the max? – Andriy Berestovskyy Mar 17 '18 at 10:58
  • @AndriyBerestovskyy how do we know that the line would necessarily pass through two rectangles' corners? – גלעד ברקן Mar 18 '18 at 01:14
  • 1
    for dynamic programming to be relevant, you need to frame the question in such a way that it can be split into overlapping subproblems, and where the optimal solutions to those subproblems can be used to generate an optimal solution for the problem as a whole. I don't know if this satisfies that requirement. – avigil Mar 18 '18 at 01:47
  • @גלעדברקן we don't, but if we need to find the max number of intersections, the corner case would be when the line touches a corner I guess – Andriy Berestovskyy Mar 18 '18 at 06:28
  • 1
    @גלעדברקן if a line doesn't pass through two corners, we can always wiggle it a bit without changing the number of intersections. – n. m. could be an AI Mar 18 '18 at 06:44
  • @n.m. good point. – גלעד ברקן Mar 18 '18 at 11:58
  • going to throw a couple of ideas: 1. would running a PCA through the centres of the boxes give you a decent enough approximation work ? 2. how about a genetic algorithm that has the line's centre and angle as the phenotypes and the max number of boxes it intersects as the fitness rule work (guessing starting at the centre of the box cluster anr rotating might converge faster ?) ? – George Profenza Mar 22 '18 at 01:18
  • Just an idea, but could you frame this in terms of linear regression?Each rectangle being composed of simple points, and then fit a 1st order polynomial (a line) to the scatter plot. This may give us a line that passes through the most points.. –  Mar 23 '18 at 23:03
  • Check idea of voting procedure in [Hough transform](https://en.wikipedia.org/wiki/Hough_transform). – Ante Mar 23 '18 at 23:17
  • You can simplify the problem by dividing the area into blocks & count how many rectangles in each, the solution is a line that cross through as many high-density areas as possible. https://i.stack.imgur.com/lrDmC.png – Khaled.K Mar 25 '18 at 09:00

6 Answers6

8

Here is a sketch of an O(n^2 log n) solution.

First, the preliminaries shared with other answers. When we have a line passing through some rectangles, we can translate it to any of the two sides until it passes through a corner of some rectangle. After that, we fix that corner as the center of rotation and rotate the line to any of the two sides until it passes through another corner. During the whole process, all points of intersection between our line and rectangle sides stayed on these sides, so the number of intersections stayed the same, as did the number of rectangles crossed by the line. As a result, we can consider only lines which pass through two rectangle corners, which is capped by O(n^2), and is a welcome improvement compared to the infinite space of arbitrary lines.

So, how do we efficiently check all these lines? First, let us have an outer loop which fixes one point A and then considers all lines passing through A. There are O(n) choices of A.

Now, we have one point A fixed, and want to consider all lines AB passing through all other corners B. In order to do that, first sort all other corners B according to the polar angle of AB, or, in other words, angle between axis Ox and vector AB. Angles are measured from -PI to +PI or from 0 to 2 PI or otherwise, the point in which we cut the circle to sort angles can be arbitrary. The sorting is done in O(n log n).

Now, we have points B1, B2, ..., Bk sorted by the polar angle around point A (their number k is something like 4n-4, all corners of all rectangles except the one where point A is a corner). First, look at the line AB1 and count the number of rectangles crossed by that line in O(n). After that, consider rotating AB1 to AB2, then AB2 to AB3, all the way to ABk. The events which happen during the rotation are as follows:

  • When we rotate to ABi, and Bi is the first corner of some rectangle in our order, the number of rectangles crossed increases by 1 as soon as the rotating line hits Bi.

  • When we rotate to ABj, and Bj is the last corner of some rectangle in our order, the number of rectangles crossed decreases by 1 as soon as the line rotates past Bj.

Which corners are first and last can be established with some O(n) preprocessing, after the sort, but before considering the ordered events.

In short, we can rotate to the next such event and update the number of rectangles crossed in O(1). And there are k = O(n) events in total. What's left to do is to track the global maximum of this quantity throughout the whole algorithm. The answer is just this maximum.

The whole algorithm runs in O(n * (n log n + n + n)), which is O(n^2 log n), just as advertised.

Gassa
  • 8,546
  • 3
  • 29
  • 49
  • 1
    Nice solution! It's along the lines of what I was thinking but solves it much more elegantly. – גלעד ברקן Mar 22 '18 at 10:43
  • 5
    Time complexity may be reduced to O(n^2) if we use "arrangements" to sort angular sequences (as explained [here](http://www.cs.wustl.edu/~pless/506/l21.html)). – Evgeny Kluev Mar 22 '18 at 11:27
  • 1
    @EvgenyKluev Looks nice, thanks for the pointer! I must note however that O(n^2) memory needed for the O(n^2) time algorithm could in practice be infeasible, or at least slow enough so that it doesn't perform better than O(n^2 log n) time solution with only O(n) memory. – Gassa Mar 22 '18 at 11:46
  • 1
    That is very cool! Would you be able to share pseudocode, just for fun? I'll wait until the end as @EvgenyKluev pointed out that a O(n^2) algorithm exists, but that's definitely the top answer at that point. – Olivier Melançon Mar 22 '18 at 16:52
  • @OlivierMelançon I've got a feeling that **pseudo**code won't add much, since the text already resembles it. On the other side, **real** code may have too much details overcasting the main flow, like dealing with rectangles located inside one another (if point A is completely inside rectangle R, then R should not contribute any corners to the sequence B), so I'm not sure if it would be a useful contribution either. – Gassa Mar 23 '18 at 11:02
4

(Edit of my earlier answer that considered rotating the plane.)

Here's sketch of the O(n^2) algorithm, which combines Gassa's idea with Evgeny Kluev's reference to dual line arrangements as sorted angular sequences.

We start out with a doubly connected edge list or similar structure, allowing us to split an edge in O(1) time, and a method to traverse the faces we create as we populate a 2-dimensional plane. For simplicity, let's use just three of the twelve corners on the rectangles below:

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

We insert the three points (corners) in the dual plane according to the following transformation:

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

Let's enter the points in order A, B, C. We first enter A => y = x - 1. Since there is only one edge so far, we insert B => y = 5x - 3, which creates the vertex, (1/2, -1/2) and splits our edge. (One elegant aspect of this solution is that each vertex (point) in the dual plane is actually the dual point of the line passing through the rectangles' corners. Observe 1 = 1/2*1 + 1/2 and 3 = 1/2*5 + 1/2, points (1,1) and (5,3).)

Entering the last point, C => y = 4x - 6, we now look for the leftmost face (could be an incomplete face) where it will intersect. This search is O(n) time since we have to try each face. We find and create the vertex (-3, -18), splitting the lower edge of 5x - 3 and traverse up the edges to split the right half of x - 1 at vertex (5/3, 2/3). Each insertion has O(n) time since we must first find the leftmost face, then traverse each face to split edges and mark the vertices (intersection points for the line).

In the dual plane we now have:

enter image description here

After constructing the line arrangement, we begin our iteration on our three example points (rectangle corners). Part of the magic in reconstructing a sorted angular sequence in relation to one point is partitioning the angles (each corresponding with an ordered line intersection in the dual plane) into those corresponding with a point on the right (with a greater x-coordinate) and those on the left and concatenating the two sequences to get an ordered sequence from -90 deg to -270 degrees. (The points on the right transform to lines with positive slopes in relation to the fixed point; the ones on left, with negative slopes. Rotate your sevice/screen clockwise until the line for (C*) 4x - 6 becomes horizontal and you'll see that B* now has a positive slope and A* negative.)

Why does it work? If a point p in the original plane is transformed into a line p* in the dual plane, then traversing that dual line from left to right corresponds with rotating a line around p in the original plane that also passes through p. The dual line marks all the slopes of this rotating line by the x-coordinate from negative infinity (vertical) to zero (horizontal) to infinity (vertical again).

(Let's summarize the rectangle-count-logic, updating the count_array for the current rectangle while iterating through the angular sequence: if it's 1, increment the current intersection count; if it's 4 and the line is not directly on a corner, set it to 0 and decrement the current intersection count.)

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)

We can see that (5,9) is above the line through AC (y = 5/3x - 2/3), which means at this point we would have counted the intersection with the rightmost rectangle and not yet reset the count for it, totaling 3 rectangles for this line.

We can also see in the graph of the dual plane, the other angular sequences:

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)
גלעד ברקן
  • 23,602
  • 3
  • 25
  • 61
  • 2
    IMO there is no guarantee we will find all the intersection this way. We can try 360 different angles, or we can try every 1/10 angle, or every 1/100 etc. So the algorithm will give a result with a predefined precision, but the answer will never be the exact maximum... – Andriy Berestovskyy Mar 18 '18 at 06:33
  • I think one needs to check angles between projection direction and each line connecting pairs of points that lie next to each other on the projection, and rotate by the smallest such angle. – n. m. could be an AI Mar 18 '18 at 07:09
  • @n.m. could you please explain? I'm not sure what you mean by "projection direction" and pairs that lie "next to each other." Perhaps you could post an answer? – גלעד ברקן Mar 18 '18 at 11:01
  • Since you rotate and always project on `x`, the projection direction will be `y` (after rotation). Next to each other means there's no other point between them. – n. m. could be an AI Mar 18 '18 at 11:49
  • @n.m. it seems to me that a "pair of points" where "there's no other point between them" is the same point :) I'm still not clear, could you please explain? – גלעד ברקן Mar 18 '18 at 11:54
  • @AndriyBerestovskyya I completely agree. – גלעד ברקן Mar 18 '18 at 12:01
  • I am not convinced that is perfectly correct yet, but the bounty was about to expire and this looks very promising to have a solution in O(n^2). I really hope it does work. But in any case, this use of a dual space in an extremely instereting approach. – Olivier Melançon Mar 27 '18 at 19:09
  • @OlivierMelançon I appreciate the bounty, and it was very interesting to learn about and try to explain. Credit goes to Gassa and Evgeny Kluev. – גלעד ברקן Mar 27 '18 at 21:36
  • @OlivierMelançon (I think if you had left the bounty, half may have gone to the highest voted answer. https://stackoverflow.com/help/bounty) – גלעד ברקן Mar 27 '18 at 21:38
4

Solution

In the space of all lines in the graph, the lines which pass by a corner are exactly the ones where the number or intersections is about to decrease. In other words, they each form a local maximum.

And for every line which passes by at least one corner, there exist an associated line that passes by two corners that has the same number of intersections.

The conclusion is that we only need to check the lines formed by two rectangle corners as they form a set that fully represents the local maxima of our problem. From those we pick the one which has the most intersections.

Time complexity

This solution first needs to recovers all lines that pass by two corners. The number of such line is O(n^2).

We then need to count the number of intersections between a given line and a rectangle. This can obviously be done in O(n) by comparing to each rectangles.

There might be a more efficient way to proceed, but we know that this algorithm is then at most O(n^3).

Python3 implementation

Here is a Python implementation of this algorithm. I oriented it more toward readability than efficiency, but it does exactly what the above defines.

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

This implementation uses the following helpers.

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

And here are the class definition that serve as data structure for rectangles and lines.

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)

Example

Here is a working example of the above code.

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)
#
#  *
#  *   
#
# **     **
# **     **
#
# **
# **

We can clearly see that an optimal solution should find a line that passes by three rectangles and that is indeed what it outputs.

print('{} with {} intersections'.format(*get_best_line(rectangles)))
# prints: y = 0.50 * x + -5.00 with 3 intersections
Community
  • 1
  • 1
Olivier Melançon
  • 21,584
  • 4
  • 41
  • 73
  • 1
    This is a straightforward brute force solution. If this were acceptable, the problem probably wouldn't be called a *challenge*. – n. m. could be an AI Mar 18 '18 at 18:36
  • 1
    I will improve it if I find a better way, I just haven't yet. Any suggestion? Plus it's not brute force since it really reduced the problem to a subset of the linear functions space. – Olivier Melançon Mar 18 '18 at 18:48
  • I think there is a better way but it is definitely not easy. I haven't quite nailed it down yet. It involves projecting all the rectangles on a line, rotating that line, and counting interval overlaps at each angle. The trick is how to go from one rotation angle to the next efficiently without recalculating everything. – n. m. could be an AI Mar 18 '18 at 19:01
  • I already tried that. It turns out that finding the projection is equivalent to projecting every point on the line at a given angle. Then what you want to do is find critical angles and calculate the projection there, but it turns out that those critical angles are defined by angles between corners. So this solution is equivalent to the one I provided, but not as readable in my opinion. Also, I do not belive you can go without recalculating a projection from the neighboring ones since projection is not injective. You are losing information in the projection. – Olivier Melançon Mar 18 '18 at 19:18
  • But I would not be surprised that there is some approach that beats O(n^3), I just don't think this is the right one. – Olivier Melançon Mar 18 '18 at 19:23
  • The projection approach could benefit from collapsing a rectangle to its outer corners that form its "silhouette" at a given angle. This forms a projected interval from which we need to count the intersections with the other rectangles. – Reblochon Masque Mar 19 '18 at 06:07
  • 1
    @n.m. and Olivier, I added an `O(n^2 (log n + m))` answer. What do you think? – גלעד ברקן Mar 22 '18 at 00:00
3

How about the following algorithm:

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

In other words, start drawing lines from each rectangle corner to each other rectangle corner and find the maximum number of intersections. As suggested by @weston, we can avoid calculating same line twice by starting inner loop from the corner next to A.

Andriy Berestovskyy
  • 8,059
  • 3
  • 17
  • 33
2

If you consider a rotating line at angle Θ and if you project all rectangles onto this line, you obtain N line segments. The maximum number of rectangles crossed by a perpendicular to this line is easily obtained by sorting the endpoints by increasing abscissa and keeping a count of the intervals met from left to right (keep a trace of whether an endpoint is a start or an end). This is shown in green.

Now two rectangles are intersected by all the lines at an angle comprised between the two internal tangents [example in red], so that all "event" angles to be considered (i.e. all angles for which a change of count can be observed) are these N(N-1) angles.

Then the brute force resolution scheme is

  • for all limit angles (O(N²) of them),

    • project the rectangles on the rotating line (O(N) operations),

    • count the overlaps and keep the largest (O(N Log N) to sort, then O(N) to count).

This takes in total O(N³Log N) operations.

enter image description here

Assuming that the sorts needn't be re-done in full for every angle if we can do them incrementally, we can hope for a complexity lowered to O(N³). This needs to be checked.


Note:

The solutions that restrict the lines to pass through the corner of one rectangle are wrong. If you draw wedges from the four corners of a rectangle to the whole extent of another, there will remain empty space in which can lie a whole rectangle that won't be touched, even though there exists a line through the three of them.

enter image description here

  • Added an `O(n^2 (log n + m))` answer. What do you think? – גלעד ברקן Mar 22 '18 at 02:23
  • @גלעדברקן: considering only the lines through one of the corners may miss better solutions. And you are giving no justification on the complexity. –  Mar 22 '18 at 08:03
  • 1
    First, (we're not considering lines, we are considering arcs and) any line that is a solution and does not pass through any corner can be rotated slightly to touch a corner. And secondly, complexity is accounted for, 4 corners * n rectangles * 2 * (log n + m) for each search and insertion in an interval tree. – גלעד ברקן Mar 22 '18 at 09:22
  • @גלעדברקן: we do consider lines and "slightly rotating" can cause some intersections to disappear. You don't even define m. –  Mar 22 '18 at 09:28
  • 1
    Can you think of an example of a solution line that cannot be rotated to touch a corner? It makes no sense. If a line is not touching any corner, rotate it until the first corner it touches. Such a motion by definition will keep all current intersections. – גלעד ברקן Mar 22 '18 at 09:40
  • If you read any interval tree lookup complexity you will know what m stands for. Your down vote is not warranted. – גלעד ברקן Mar 22 '18 at 09:42
  • The example you added of arcs of lines that pass through corners that miss a rectangle is a good example of lines that aren't solution lines. A line that is a solution line in that example can be made to pass through a corner of the small middle rectangle. – גלעד ברקן Mar 22 '18 at 10:40
  • @גלעדברקן: do you mean line segments ? –  Mar 22 '18 at 10:43
  • Not sure why that differentiation would matter here :) What do you mean? – גלעד ברקן Mar 22 '18 at 10:45
1

We can have an O(n^2 (log n + m)) dynamic-programming method by adapting Andriy Berestovskyy's idea of iterating over the corners slightly to insert the relationship of the current corner vis a vis all the other rectangles into an interval tree for each of our 4n iteration cycles.

A new tree will be created for the corner we are trying. For each rectangle's four corners we'll iterate over each of the other rectangles. What we'll insert will be the angles marking the arc the paired-rectangle's farthest corners create in relation to the current fixed corner.

In the example directly below, for the fixed lower rectangle's corner R when inserting the record for the middle rectangle, we would insert the angles marking the arc from p2 to p1 in relation to R (about (37 deg, 58 deg)). Then when we check the high rectangle in relation to R, we'll insert the interval of angles marking the arc from p4 to p3 in relation to R (about (50 deg, 62 deg)).

When we insert the next arc record, we'll check it against all intersecting intervals and keep a record of the most intersections.

enter image description here

(Note that because any arc on a 360 degree circle for our purpose has a counterpart rotated 180 degrees, we may need to make an arbitrary cutoff (any alternative insights would be welcome). For example, this means that an arc from 45 degrees to 315 degrees would split into two: [0, 45] and [135, 180]. Any non-split arc could only intersect with one or the other but either way, we may need an extra hash to make sure rectangles are not double-counted.)

גלעד ברקן
  • 23,602
  • 3
  • 25
  • 61