Bentley's rectangle union sweep in Python
Sweep over vertical edges with a segment tree over compressed y-coordinates to compute union area.
- File
rectangle_union_sweep.py, 69 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/rectangle-union-sweep/. The authors are listed in prov/4TVWVM.jsonl.
from typing import Sequence
Rect = tuple[float, float, float, float] # (x1, y1, x2, y2) with x1 <= x2 and y1 <= y2
class CoverTree:
"""Segment tree over the elementary intervals [ys[j], ys[j + 1]], j = 0 .. m - 2.
cnt[u] counts the active intervals that were stored at node u (they cover
the range of u and not the range of its parent). length[u] is the length of
the part of the range of u that the active intervals cover, counting only
the intervals stored at u and below u.
"""
def __init__(self, ys: list[float]) -> None:
self.ys = ys
size = max(1, 4 * (len(ys) - 1))
self.cnt = [0] * size
self.length = [0] * size
def covered(self) -> float:
return self.length[1] if len(self.ys) >= 2 else 0
def update(self, lo: int, hi: int, v: int) -> None:
"""Add v (+1 or -1) to the elementary intervals with index in [lo, hi)."""
if lo < hi:
self._update(1, 0, len(self.ys) - 1, lo, hi, v)
def _update(self, u: int, l: int, r: int, lo: int, hi: int, v: int) -> None:
if hi <= l or r <= lo: # @line L11
return
if lo <= l and r <= hi: # @line L12
self.cnt[u] += v
else: # @line L13
mid = (l + r) // 2
self._update(2 * u, l, mid, lo, hi, v)
self._update(2 * u + 1, mid, r, lo, hi, v)
if self.cnt[u] > 0: # @line L14
self.length[u] = self.ys[r] - self.ys[l]
elif r - l == 1: # @line L15
self.length[u] = 0
else: # @line L16
self.length[u] = self.length[2 * u] + self.length[2 * u + 1]
def union_area(rects: Sequence[Rect]) -> float:
"""Return the area of the union of the closed axis-parallel rectangles.
Each rectangle is (x1, y1, x2, y2). The result is exact (an int) for
integer input. Time O(n log n), extra space O(n).
"""
ys = sorted({r[1] for r in rects} | {r[3] for r in rects}) # @line L1
index = {y: j for j, y in enumerate(ys)}
events = []
for x1, y1, x2, y2 in rects: # @line L2
events.append((x1, 1, y1, y2))
events.append((x2, -1, y1, y2))
events.sort() # @line L3
tree = CoverTree(ys) # @line L4
area = 0 # @line L5
prev = events[0][0] if events else 0
for x, v, y1, y2 in events: # @line L6
area += tree.covered() * (x - prev) # @line L7
tree.update(index[y1], index[y2], v) # @line L8
prev = x # @line L9
return area # @line L10
test_rectangle_union_sweep.py 48 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/rectangle-union-sweep/. The authors are listed in prov/4TVWVM.jsonl.
import random
from rectangle_union_sweep import union_area
def brute(rects):
"""Sum the cells of the grid of all coordinates whose center lies in some rectangle."""
xs = sorted({r[0] for r in rects} | {r[2] for r in rects})
ys = sorted({r[1] for r in rects} | {r[3] for r in rects})
total = 0
for i in range(len(xs) - 1):
for j in range(len(ys) - 1):
cx, cy = (xs[i] + xs[i + 1]) / 2, (ys[j] + ys[j + 1]) / 2
if any(r[0] <= cx <= r[2] and r[1] <= cy <= r[3] for r in rects):
total += (xs[i + 1] - xs[i]) * (ys[j + 1] - ys[j])
return total
def test_example() -> None:
assert union_area([(1, 1, 4, 3), (2, 2, 6, 5), (5, 0, 7, 2)]) == 20
def test_edges() -> None:
assert union_area([]) == 0
assert union_area([(0, 0, 3, 2)]) == 6
assert union_area([(0, 0, 3, 2), (0, 0, 3, 2)]) == 6 # identical
assert union_area([(0, 0, 10, 10), (2, 2, 3, 3)]) == 100 # nested
assert union_area([(0, 0, 1, 1), (1, 0, 2, 1)]) == 2 # touching edge
assert union_area([(0, 0, 0, 5), (1, 1, 1, 1)]) == 0 # degenerate
assert union_area([(0, 4, 10, 5), (4, 0, 5, 10)]) == 19 # cross
big = 10**12
assert union_area([(0, 0, big, big), (big // 2, 0, 2 * big, big)]) == 2 * big * big
assert abs(union_area([(0.0, 0.0, 0.5, 0.5), (0.25, 0.25, 1.0, 1.0)]) - 0.75) < 1e-12
def test_against_brute_force() -> None:
rng = random.Random(11)
for _ in range(1500):
rects = []
for _ in range(rng.randrange(0, 8)):
x1, y1 = rng.randrange(10), rng.randrange(10)
rects.append((x1, y1, x1 + rng.randrange(0, 5), y1 + rng.randrange(0, 5)))
got = union_area(rects)
assert got == brute(rects)
assert isinstance(got, int)
What the next level needs
- the tests did not run in this build