Algorithm Search⌘K

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
rectangle_union_sweep.pyRawGitHub
# 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

Read the full page about Bentley's rectangle union sweep