# Source code for tryalgo.union_rectangles

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""\
Union of rectangles
jill-jênn vie et christoph dürr - 2014-2019
"""
# pylint: disable=too-many-arguments, too-many-locals

# snip{ union_intervals
from collections import Counter
# snip}

# weighted variant of tryalgo.range_minimum_query.LazySegmentTree
# snip{ cover-query
[docs]class CoverQuery:
"""Segment tree to maintain a set of integer intervals
and permitting to query the size of their union.
"""
def __init__(self, L):
"""creates a structure, where all possible intervals
will be included in [0, L - 1].
"""
assert L != []              # L is assumed sorted
self.N = 1
while self.N < len(L):
self.N *= 2
self.c =  * (2 * self.N)         # --- covered
self.s =  * (2 * self.N)         # --- score
self.w =  * (2 * self.N)         # --- length
for i, _ in enumerate(L):
self.w[self.N + i] = L[i]
for p in range(self.N - 1, 0, -1):
self.w[p] = self.w[2 * p] + self.w[2 * p + 1]

[docs]    def cover(self):
""":returns: the size of the union of the stored intervals
"""
return self.s

[docs]    def change(self, i, k, offset):
"""when offset = +1, adds an interval [i, k],
when offset = -1, removes it
:complexity: O(log L)
"""
self._change(1, 0, self.N, i, k, offset)

def _change(self, p, start, span, i, k, offset):
if start + span <= i or k <= start:   # --- disjoint
return
if i <= start and start + span <= k:  # --- included
self.c[p] += offset
else:
self._change(2 * p, start, span // 2, i, k, offset)
self._change(2 * p + 1, start + span // 2, span // 2,
i, k, offset)
if self.c[p] == 0:
if p >= self.N:                   # --- leaf
self.s[p] = 0
else:
self.s[p] = self.s[2 * p] + self.s[2 * p + 1]
else:
self.s[p] = self.w[p]
# snip}

# snip{ union_intervals
OPENING = +1  # constants for events
CLOSING = -1  # -1 has higher priority

[docs]def union_intervals(intervals):
"""Size of the union of a set of intervals

Sweep from left to right.
Maintain in a counter number of opened intervals
minus number of closed intervals.

:param intervals: Counter, which describes a multiset of intervals.
an interval is a pair of values.
:returns: size of the union of those intervals
:complexity: :math:O(n \\log n)
"""
union_size = 0
events = []
for x1, x2 in intervals:
for _ in range(intervals[x1, x2]):
assert x1 <= x2
events.append((x1, OPENING))
events.append((x2, CLOSING))
previous_x = 0    # arbitrary initial value
#                   ok, because opened == 0 at first event
opened = 0
for x, offset in sorted(events):
if opened > 0:
union_size += x - previous_x
previous_x = x
opened += offset
return union_size
# snip}

# snip{ union_rectangles
[docs]def union_rectangles(R):
"""Area of union of rectangles.

Sweep from top to bottom.
Maintain in a set the horizontal projection of rectangles,
for which the top border has been processed but not yet the bottom.

:param R: list of rectangles defined by (x1, y1, x2, y2)
where (x1, y1) is top left corner and (x2, y2) bottom right corner
:returns: area
:complexity: :math:O(n^2 \\log n)
"""
events = []
for x1, y1, x2, y2 in R:                     # initialize events
assert x1 <= x2 and y1 <= y2
events.append((y1, OPENING, x1, x2))
events.append((y2, CLOSING, x1, x2))
current_intervals = Counter()
area = 0
previous_y = 0  # arbitrary initial value,
#                 ok, because union_intervals is 0 at first event
for y, offset, x1, x2 in sorted(events):         # sweep top down
area += (y - previous_y) * union_intervals(current_intervals)
previous_y = y
current_intervals[x1, x2] += offset
return area
# snip}

# snip{ union_rectangles_fast
[docs]def union_rectangles_fast(R):
"""Area of union of rectangles

:param R: list of rectangles defined by (x1, y1, x2, y2)
where (x1, y1) is top left corner and (x2, y2) bottom right corner
:returns: area
:complexity: :math:O(n^2)
"""
X = set()                 # set of all x coordinates in the input
events = []               # events for the sweep line
for x1, y1, x2, y2 in R:
assert x1 <= x2 and y1 <= y2
events.append((y1, OPENING, x1, x2))
events.append((y2, CLOSING, x1, x2))
# array of x coordinates in left to right order
i_to_x = list(sorted(X))
# inverse dictionary maps x coordinate to its rank
x_to_i = {xi: i for i, xi in enumerate(i_to_x)}
# nb_current_rectangles[i] = number of rectangles intersected
# by the sweepline in interval [i_to_x[i], i_to_x[i + 1]]
nb_current_rectangles =  * (len(i_to_x) - 1)
area = 0
length_union_intervals = 0
previous_y = 0  # arbitrary initial value,
#                 because length is 0 at first iteration
for y, offset, x1, x2 in sorted(events):
area += (y - previous_y) * length_union_intervals
i1 = x_to_i[x1]
i2 = x_to_i[x2]         # update nb_current_rectangles
for j in range(i1, i2):
length_interval = i_to_x[j + 1] - i_to_x[j]
if nb_current_rectangles[j] == 0:
length_union_intervals += length_interval
nb_current_rectangles[j] += offset
if nb_current_rectangles[j] == 0:
length_union_intervals -= length_interval
previous_y = y
return area
# snip}

# snip{ union_rectangles_fastest
[docs]def union_rectangles_fastest(R):
"""Area of union of rectangles

:param R: list of rectangles defined by (x1, y1, x2, y2)
where (x1, y1) is top left corner and (x2, y2) bottom right corner
:returns: area
:complexity: :math:O(n \\log n)
"""
if R == []:               # segment tree would fail on an empty list
return 0
X = set()                 # set of all x coordinates in the input
events = []               # events for the sweep line
for Rj in R:
(x1, y1, x2, y2) = Rj
assert x1 <= x2 and y1 <= y2
events.append((y1, OPENING, x1, x2))
events.append((y2, CLOSING, x1, x2))
i_to_x = list(sorted(X))
# inverse dictionary
x_to_i = {i_to_x[i]: i for i in range(len(i_to_x))}
L = [i_to_x[i + 1] - i_to_x[i] for i in range(len(i_to_x) - 1)]
C = CoverQuery(L)
area = 0
previous_y = 0  # arbitrary initial value,
#                 because C.cover() is 0 at first iteration
for y, offset, x1, x2 in sorted(events):
area += (y - previous_y) * C.cover()
i1 = x_to_i[x1]
i2 = x_to_i[x2]
C.change(i1, i2, offset)
previous_y = y
return area
# snip}

# snip{ union_rectangles_naive
[docs]def rectangles_contains_point(R, x, y):
"""Decides if at least one of the given rectangles contains a given point
either strictly or on its left or top border
"""
for x1, y1, x2, y2 in R:
if x1 <= x < x2 and y1 <= y < y2:
return True
return False

[docs]def union_rectangles_naive(R):
"""Area of union of rectangles

:param R: list of rectangles defined by (x1, y1, x2, y2)
where (x1, y1) is top left corner and (x2, y2) bottom right corner
:returns: area
:complexity: :math:O(n^3)
"""
X = set()        # set of all x coordinates in the input
Y = set()        # same for y
for x1, y1, x2, y2 in R:
assert x1 <= x2 and y1 <= y2
j_to_x = list(sorted(X))
i_to_y = list(sorted(Y))
# X and Y partition space into a grid
area = 0
for j in range(len(j_to_x) - 1):      # loop over columns in grid
x1 = j_to_x[j]
x2 = j_to_x[j + 1]
for i in range(len(i_to_y) - 1):  # loop over rows
y1 = i_to_y[i]                # (x1,...,y2) is the grid cell
y2 = i_to_y[i + 1]
if rectangles_contains_point(R, x1, y1):
area += (y2 - y1) * (x2 - x1)  # cell is covered
return area
# snip}