Algorithm Search⌘K

Rectangle intersection sweep in Python

Sweep over x-intervals of rectangles, checking y-overlap against active rectangles.

File
rectangle_intersection_sweep.py, 65 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
rectangle_intersection_sweep.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/rectangle-intersection-sweep/. The authors are listed in prov/KGSR21.jsonl.

from bisect import bisect_left, insort
from typing import Sequence

Rect = tuple[float, float, float, float]  # (x1, y1, x2, y2) with x1 <= x2 and y1 <= y2


def find_intersecting_pair(rects: Sequence[Rect]) -> tuple[int, int] | None:
    """Return indices (i, j), i < j, of two closed rectangles that intersect, or None.

    Each rectangle is (x1, y1, x2, y2) and stands for [x1, x2] x [y1, y2].
    Rectangles that touch on an edge or a corner intersect. The sweep sorts
    2n events in O(n log n) time. The active set is a sorted Python list, so
    an insert or a delete costs O(n) moves; a balanced search tree makes each
    one O(log n) and the whole sweep O(n log n).
    """
    events = []
    for i, (x1, _, x2, _) in enumerate(rects):  # @line L1
        events.append((x1, 0, i))
        events.append((x2, 1, i))
    events.sort()  # @line L2
    active: list[tuple[float, int]] = []  # @line L3
    for x, kind, i in events:  # @line L4
        y1, y2 = rects[i][1], rects[i][3]
        key = (y1, i)
        if kind == 0:  # @line L5
            insort(active, key)  # @line L6
            pos = bisect_left(active, key)
            p = active[pos - 1][1] if pos > 0 else None  # @line L7
            s = active[pos + 1][1] if pos + 1 < len(active) else None
            if p is not None and rects[p][3] >= y1:  # @line L8
                return (min(p, i), max(p, i))
            if s is not None and rects[s][1] <= y2:  # @line L9
                return (min(i, s), max(i, s))
        else:
            del active[bisect_left(active, key)]  # @line L10
    return None  # @line L11


def all_intersecting_pairs(rects: Sequence[Rect]) -> list[tuple[int, int]]:
    """Return every pair (i, j), i < j, of intersecting closed rectangles, sorted.

    The same sweep, but each new rectangle is tested against every active
    one. This costs O(n log n + n * a) time, where a is the largest number of
    active rectangles; an interval tree gives O(n log n + k) for k pairs.
    """
    events = []
    for i, (x1, _, x2, _) in enumerate(rects):
        events.append((x1, 0, i))
        events.append((x2, 1, i))
    events.sort()
    active: set[int] = set()
    out = []
    for _, kind, i in events:
        if kind == 0:
            y1, y2 = rects[i][1], rects[i][3]
            for j in active:
                if rects[j][1] <= y2 and y1 <= rects[j][3]:
                    out.append((min(i, j), max(i, j)))
            active.add(i)
        else:
            active.discard(i)
    return sorted(out)
test_rectangle_intersection_sweep.py 55 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/rectangle-intersection-sweep/. The authors are listed in prov/KGSR21.jsonl.

import random

from rectangle_intersection_sweep import all_intersecting_pairs, find_intersecting_pair


def meets(a, b) -> bool:
    return a[0] <= b[2] and b[0] <= a[2] and a[1] <= b[3] and b[1] <= a[3]


def brute(rects):
    n = len(rects)
    return [(i, j) for i in range(n) for j in range(i + 1, n) if meets(rects[i], rects[j])]


EXAMPLE = [(1, 1, 3, 2), (2, 4, 5, 6), (4, 0, 7, 3), (6, 5, 9, 7), (8, 1, 10, 4)]


def test_example() -> None:
    assert find_intersecting_pair(EXAMPLE) is None
    moved = EXAMPLE + [(5, 3, 6, 4)]
    assert find_intersecting_pair(moved) == (2, 5)
    assert all_intersecting_pairs(moved) == [(1, 5), (2, 5)]


def test_edges() -> None:
    assert find_intersecting_pair([]) is None
    assert find_intersecting_pair([(0, 0, 1, 1)]) is None
    assert find_intersecting_pair([(0, 0, 1, 1), (1, 1, 2, 2)]) == (0, 1)  # corner touch
    assert find_intersecting_pair([(0, 0, 1, 1), (1, 2, 2, 3)]) is None
    assert find_intersecting_pair([(0, 0, 1, 1), (0, 0, 1, 1)]) == (0, 1)  # identical
    assert find_intersecting_pair([(0, 0, 10, 10), (2, 2, 3, 3)]) == (0, 1)  # nested
    # a long thin rectangle crosses a tall thin one: no corner is inside the other
    assert find_intersecting_pair([(0, 4, 10, 5), (4, 0, 5, 10)]) == (0, 1)
    # equal lower edges: the key (y1, index) breaks the tie
    assert find_intersecting_pair([(0, 2, 1, 3), (5, 2, 6, 3), (5.5, 2, 7, 2.5)]) == (1, 2)


def test_against_brute_force() -> None:
    rng = random.Random(7)
    for _ in range(2000):
        n = rng.randrange(0, 9)
        rects = []
        for _ in range(n):
            x1, y1 = rng.randrange(12), rng.randrange(12)
            rects.append((x1, y1, x1 + rng.randrange(0, 4), y1 + rng.randrange(0, 4)))
        want = brute(rects)
        got = find_intersecting_pair(rects)
        if want:
            assert got is not None and meets(rects[got[0]], rects[got[1]]) and got[0] != got[1]
        else:
            assert got is None
        assert all_intersecting_pairs(rects) == want

What the next level needs

  • the tests did not run in this build

Read the full page about Rectangle intersection sweep